From f3c854f5cb0d2305ffd36f5992b41292debbc485 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 5 Nov 2025 17:25:32 -0800 Subject: [PATCH 001/308] created documentation for modifiability checker (main stuff not done), added the section to the manual. --- docs/manual/Makefile | 1 + docs/manual/advanced-features.tex | 3 +++ docs/manual/introduction.tex | 4 ++++ docs/manual/manual.tex | 3 +++ docs/manual/modifiability-checker.tex | 5 +++++ 5 files changed, 16 insertions(+) create mode 100644 docs/manual/modifiability-checker.tex diff --git a/docs/manual/Makefile b/docs/manual/Makefile index fd54cb10849d..1eaac4e32635 100644 --- a/docs/manual/Makefile +++ b/docs/manual/Makefile @@ -26,6 +26,7 @@ signature-checker.tex \ guieffect-checker.tex \ units-checker.tex \ signedness-checker.tex \ +modifiability-checker.tex \ purity-checker.tex \ constant-value-checker.tex \ returns-receiver-checker.tex \ diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 14f3dc5da466..14608fa58a89 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -1016,6 +1016,9 @@ \item \ahrefloc{must-call-checker}{Must Call Checker} to over-approximate the methods that should be called on an object before it is de-allocated (see \chapterpageref{must-call-checker}) +\item + \ahrefloc{modifiability-checker}{Modifiability Checker} to + warn about possible \ at run time on mutatable collections. (see \chapterpageref{modifiability-checker}) \item \ahrefloc{subtyping-checker}{Subtyping Checker} for customized checking without writing any code (see \chapterpageref{subtyping-checker}) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 5ae580265fe8..e2b2c3fa6458 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -96,6 +96,10 @@ \ahrefloc{signedness-checker}{Signedness Checker} to ensure unsigned and signed values are not mixed (see \chapterpageref{signedness-checker}) +\item + \ahrefloc{modifiability-checker}{Modifiability Checker} to + warn about possible \ at run time on mutatable collections. + (see \chapterpageref{modifiability-checker}) \item \ahrefloc{purity-checker}{Purity Checker} to identify whether methods have side effects (see \chapterpageref{purity-checker}) diff --git a/docs/manual/manual.tex b/docs/manual/manual.tex index 3c6bad8703b4..99d5200adde7 100644 --- a/docs/manual/manual.tex +++ b/docs/manual/manual.tex @@ -76,6 +76,9 @@ % These are focused on ints: \input{signedness-checker.tex} +%% Checker related to UnsupportedOperationException +\input{modifiability-checker.tex} + % Checkers that tend to be used as components of other checkers rather % than used for their own sake: \input{purity-checker.tex} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex new file mode 100644 index 000000000000..926e1821c1ca --- /dev/null +++ b/docs/manual/modifiability-checker.tex @@ -0,0 +1,5 @@ +\chapter{Modifiable Checker\label{modifiable-checker}} + +The Modifiable Checker warns, at compile time, if a program might throw +\ at run time due to calling a mutating method +on an unmodifiable collection. \ No newline at end of file From e647763fe4ff9ecf763438b91d4597b0095a059f Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 5 Nov 2025 18:21:24 -0800 Subject: [PATCH 002/308] edit the modifiability doc so that make html build successed --- docs/manual/contributors.tex | 1 + docs/manual/modifiability-checker.tex | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/manual/contributors.tex b/docs/manual/contributors.tex index df1399d9c7b5..36e6edd8154a 100644 --- a/docs/manual/contributors.tex +++ b/docs/manual/contributors.tex @@ -151,4 +151,5 @@ Vlastimil Dort, Weitian Xing, Werner Dietl, +Yuki Yang, Zhiping Cai. diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 926e1821c1ca..874d55e5420b 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -1,5 +1,5 @@ -\chapter{Modifiable Checker\label{modifiable-checker}} +\chapterAndLabel{Modifiability Checker}{modifiability-checker} -The Modifiable Checker warns, at compile time, if a program might throw +The Modifiability Checker warns, at compile time, if a program might throw \ at run time due to calling a mutating method on an unmodifiable collection. \ No newline at end of file From 4a1b7df6c769923f2770faee80bc57cbe0893e34 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 10 Nov 2025 00:32:19 -0800 Subject: [PATCH 003/308] Moved the modifiability checker to "type system that do not currently support a run-time test, but could do so with add. imp." --- docs/manual/advanced-features.tex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 14608fa58a89..e7c0fdbc7577 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -972,6 +972,9 @@ \ahrefloc{constant-value-checker}{Constant Value Checker} to determine whether an expression's value can be known at compile time (see \chapterpageref{constant-value-checker}) +\item + \ahrefloc{modifiability-checker}{Modifiability Checker} to + warn about possible \ at run time on mutatable collections. (see \chapterpageref{modifiability-checker}) \end{itemize} @@ -1016,9 +1019,6 @@ \item \ahrefloc{must-call-checker}{Must Call Checker} to over-approximate the methods that should be called on an object before it is de-allocated (see \chapterpageref{must-call-checker}) -\item - \ahrefloc{modifiability-checker}{Modifiability Checker} to - warn about possible \ at run time on mutatable collections. (see \chapterpageref{modifiability-checker}) \item \ahrefloc{subtyping-checker}{Subtyping Checker} for customized checking without writing any code (see \chapterpageref{subtyping-checker}) From 4b7e5ae0b9de784f526c7999335e98e8e070b751 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 10 Nov 2025 01:06:12 -0800 Subject: [PATCH 004/308] finished the modifiability checker manual --- docs/manual/modifiability-checker.tex | 181 +++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 3 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 874d55e5420b..1f5f449ae883 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -1,5 +1,180 @@ \chapterAndLabel{Modifiability Checker}{modifiability-checker} -The Modifiability Checker warns, at compile time, if a program might throw -\ at run time due to calling a mutating method -on an unmodifiable collection. \ No newline at end of file +The Modifiability Checker issues a warning, at compile time, if a program might +throw \code{UnsupportedOperationException} at run time due to invoking a +mutating method (such as \code{add} or \code{remove}) on an unmodifiable +collection. + +An \emph{unmodifiable collection} is one that throws +\code{UnsupportedOperationException} if a mutating method is called. +The JDK provides many such collections, for example, the results of: +\begin{itemize} + \item \code{List.of()}, \code{List.copyOf()} + \item \code{Set.of()}, \code{Set.copyOf()} + \item \code{Map.of()}, \code{Map.copyOf()} + \item \code{Collections.emptyList()}, \code{Collections.emptySet()}, \code{Collections.emptyMap()} + \item \code{Collections.unmodifiableList()}, \code{Collections.unmodifiableSet()}, \code{Collections.unmodifiableMap()} +\end{itemize} + +A \emph{modifiable collection} is one on which mutating methods can be called +without throwing \code{UnsupportedOperationException}. Many other methods, such +as \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be +called on any collection, whether modifiable or unmodifiable. + +The Modifiability Checker classifies each collection (and iterator) value by its +\emph{modifiability capability}. It guarantees that mutating methods are only +called on collections that are known to be modifiable. Thus, it prevents a +common class of runtime bugs where code compiles but fails at execution with +\code{UnsupportedOperationException}. + +To run the Modifiability Checker: + +\begin{Verbatim} +javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java +\end{Verbatim} + +You can also run it together with other checkers by listing multiple processors +to \code{-processor}. + +\sectionAndLabel{Modifiability annotations}{modifiability-annotations} + +The Modifiability Checker provides a small lattice of type qualifiers that +describe the modifiability capability of a collection. The main user-visible +qualifiers are: +\begin{itemize} + \item \code{@UnknownModifiability}, + \item \code{@Modifiable}, and + \item \code{@Unmodifiable}. +\end{itemize} +Internally, the checker also uses a bottom qualifier +\code{@BottomModifiable}, which programmers do not write. + +The qualifier hierarchy is shown in +Figure~\ref{fig-modifiability-lattice}. + +\begin{figure} + \centering + \begin{minipage}{0.5\linewidth} +\begin{verbatim} + @UnknownModifiability + / \ +@Modifiable @Unmodifiable + \ / + @BottomModifiable +\end{verbatim} + \end{minipage} + \caption{Modifiability qualifier hierarchy.} + \label{fig-modifiability-lattice} +\end{figure} + +\begin{description} + +\item[\code{@UnknownModifiability}] +The top qualifier in the hierarchy. The checker does not know whether the +collection is modifiable or unmodifiable; it may be either. This is the default +qualifier that is applied to unannotated collection types. + +\item[\code{@Modifiable}] +The collection is known to support all standard mutating operations, such as +\code{add}, \code{remove}, \code{clear}, and \code{put}, without throwing +\code{UnsupportedOperationException}. A call to a mutating method is permitted +only when the receiver is annotated as \code{@Modifiable}. + +\item[\code{@Unmodifiable}] +The collection is known to be unmodifiable: any mutating operation would throw +\code{UnsupportedOperationException}. A call to a mutating method on a +\code{@Unmodifiable} receiver always yields a type-checking error. + +\item[\code{@BottomModifiable}] +The bottom qualifier in the hierarchy. It is a subtype of both +\code{@Modifiable} and \code{@Unmodifiable}. It is used internally by the +checker and is not intended to be written by programmers. + +\end{description} + +Each annotation’s Javadoc should include a precise description and an example +use, and should link back to this chapter using +\code{@checker\_framework.manual \#modifiability-checker Modifiability Checker}. +Internal qualifiers such as \code{@BottomModifiable} should be documented last. + +\sectionAndLabel{What the Modifiability Checker checks}{modifiability-rules} + +The Modifiability Checker enforces that mutating methods are only called on +collections that are definitely modifiable. + +A \emph{mutating method} is any method that may change the size or contents of a +collection, such as: +\begin{itemize} + \item \code{add}, \code{addAll} + \item \code{remove}, \code{removeAll}, \code{retainAll}, \code{clear} + \item \code{put}, \code{putAll}, \code{replace}, \code{replaceAll} +\end{itemize} + +A \emph{read-only method} is any method that does not modify the collection, +such as: +\begin{itemize} + \item \code{contains}, \code{containsAll} + \item \code{equals}, \code{hashCode} + \item \code{get}, \code{getFirst}, \code{getLast} + \item \code{indexOf}, \code{isEmpty}, \code{toArray} +\end{itemize} + +The checker enforces the following rules: + +\begin{itemize} + \item A call to a mutating method is permitted only if the receiver is + annotated as \code{@Modifiable}. + \item If the receiver is annotated as \code{@Unmodifiable}, the checker issues + an error because the call would always throw + \code{UnsupportedOperationException}. + \item If the receiver is annotated as \code{@UnknownModifiability}, the checker + conservatively issues a warning, because the collection might be + unmodifiable at run time. + \item Read-only methods may be called on collections of any modifiability + qualifier (\code{@UnknownModifiability}, \code{@Modifiable}, + \code{@Unmodifiable}) without error. + \item Subtyping follows the lattice in + Figure~\ref{fig-modifiability-lattice}: both \code{@Modifiable} and + \code{@Unmodifiable} are subtypes of \code{@UnknownModifiability}, and + \code{@BottomModifiable} is a subtype of both. +\end{itemize} + +The checker does not rely on run-time tests or exceptions; it reasons entirely +from static types. Methods that are known to return unmodifiable collections +(such as \code{List.of()} and \code{Collections.unmodifiableList(...)}) should +be annotated in the JDK as returning \code{@Unmodifiable}, so that the checker +can track modifiability through library calls. + +\sectionAndLabel{Examples}{modifiability-examples} + +\begin{Verbatim} +import java.util.*; + +class Demo { + + void factoryExamples() { + @Modifiable List a = new ArrayList<>(); + @Unmodifiable List b = List.of("x", "y"); + @UnknownModifiability List c = maybeList(); + + a.add("z"); // OK: receiver is @Modifiable + + b.add("z"); // error: mutating @Unmodifiable list + + c.add("z"); // error: warning! possible mutation of a collection whose + // modifiability is unknown + } + + @UnknownModifiability List maybeList() { + // Might return modifiable or unmodifiable list. + return Math.random() > 0.5 ? List.of("a") : new ArrayList<>(); + } + + void readonlyExamples(@UnknownModifiability List x, + @Unmodifiable List y) { + x.contains("a"); // OK + y.get(0); // OK + // Read-only methods are safe for any qualifier. + } +} +\end{Verbatim} \ No newline at end of file From 4cae78e14ee95cf2c3bbf9d86e96deac004fabbf Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 10 Nov 2025 01:09:37 -0800 Subject: [PATCH 005/308] added todo for the manual --- docs/manual/modifiability-checker.tex | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 1f5f449ae883..01be735cd6bf 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -52,6 +52,8 @@ The qualifier hierarchy is shown in Figure~\ref{fig-modifiability-lattice}. + +% TODO: add a figure that follows the sturucture of other checker manuals. \begin{figure} \centering \begin{minipage}{0.5\linewidth} @@ -92,10 +94,11 @@ \end{description} -Each annotation’s Javadoc should include a precise description and an example -use, and should link back to this chapter using -\code{@checker\_framework.manual \#modifiability-checker Modifiability Checker}. -Internal qualifiers such as \code{@BottomModifiable} should be documented last. +% TODO: +% Each annotation’s Javadoc should include a precise description and an example +% use, and should link back to this chapter using +% \code{@checker\_framework.manual \#modifiability-checker Modifiability Checker}. +% Internal qualifiers such as \code{@BottomModifiable} should be documented last. \sectionAndLabel{What the Modifiability Checker checks}{modifiability-rules} From 94ab0ffbd3d80c5777cae32c2bcb61383132ee77 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 10 Nov 2025 14:17:33 -0800 Subject: [PATCH 006/308] adding qualifiers --- .../modifiability/qual/BottomModifiable.java | 25 +++++++++++++++++++ .../modifiability/qual/Modifiable.java | 20 +++++++++++++++ .../qual/UnknownModifiability.java | 24 ++++++++++++++++++ .../modifiability/qual/Unmodifiable.java | 20 +++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java new file mode 100644 index 000000000000..56fbf741771f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.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.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + +/** + * The bottom qualifier for the modifiability hierarchy. It is a subtype of both {@link Modifiable} + * and {@link Unmodifiable}. This qualifier is used internally by the Checker Framework to represent + * unreachable or contradictory states. + * + *

Programmers should not write this annotation explicitly. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Modifiable.class, Unmodifiable.class}) +@InvisibleQualifier +public @interface BottomModifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java new file mode 100644 index 000000000000..c10f11f478b6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.modifiability.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 collection is known to support all mutating operations such as {@code add}, {@code remove}, + * {@code clear}, etc., without throwing {@link UnsupportedOperationException}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownModifiability.class) +public @interface Modifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java new file mode 100644 index 000000000000..d54e879a7219 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Represents an unknown or arbitrary modifiability capability. The checker cannot determine whether + * the collection is modifiable or unmodifiable. This is the top qualifier in the hierarchy. + * + *

This is the default qualifier for unannotated types. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface UnknownModifiability {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java new file mode 100644 index 000000000000..b4decae44783 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.modifiability.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 collection is known to be unmodifiable: calling a mutating method such as {@code add} or + * {@code remove} will always throw {@link UnsupportedOperationException}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownModifiability.class) +public @interface Unmodifiable {} From 25e3436bbb20e967c09de08d3e8cc859d74eba2e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 10 Nov 2025 14:18:31 -0800 Subject: [PATCH 007/308] added HierarchyTest --- .../tests/modifiability/HierarchyTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 checker/tests/modifiability/HierarchyTest.java diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java new file mode 100644 index 000000000000..6fe9ee014568 --- /dev/null +++ b/checker/tests/modifiability/HierarchyTest.java @@ -0,0 +1,36 @@ +import org.checkerframework.checker.modifiability.qual.BottomModifiable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +class HierarchyTest { + + void testHierarchy( + @UnknownModifiability Object any, @Modifiable Object mod, @Unmodifiable Object unmod) { + + // --- Assignments to UnknownModifiability (top) --- + @UnknownModifiability Object a1 = any; + @UnknownModifiability Object a2 = mod; + @UnknownModifiability Object a3 = unmod; // OK: both subtypes of UnknownModifiability + + // --- Assignments to Modifiable --- + @Modifiable Object m1 = mod; // OK + // :: error: (assignment) + @Modifiable Object m2 = any; // error: UnknownModifiability → Modifiable not allowed + // :: error: (assignment) + @Modifiable Object m3 = unmod; // error: Unmodifiable → Modifiable not allowed + + // --- Assignments to Unmodifiable --- + @Unmodifiable Object u1 = unmod; // OK + // :: error: (assignment) + @Unmodifiable Object u2 = any; // error: UnknownModifiability → Unmodifiable not allowed + // :: error: (assignment) + @Unmodifiable Object u3 = mod; // error: Modifiable → Unmodifiable not allowed + + // --- Reflexivity (bottom always OK) --- + @BottomModifiable Object b = null; + @Modifiable Object m4 = b; // OK: BottomModifiable <: Modifiable + @Unmodifiable Object u4 = b; // OK: BottomModifiable <: Unmodifiable + @UnknownModifiability Object a4 = b; // OK: BottomModifiable <: UnknownModifiability + } +} From f8313384f4662c2f6f7c19e31537e986122212fd Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 10 Nov 2025 17:16:33 -0800 Subject: [PATCH 008/308] add polyModifiable type --- .../modifiability/qual/PolyModifiable.java | 27 +++++++++++++++++++ .../modifiability/PolyModifiableTest.java | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java create mode 100644 checker/tests/modifiability/PolyModifiableTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java new file mode 100644 index 000000000000..afdb2fd582d0 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.modifiability.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.PolymorphicQualifier; + +/** + * A polymorphic qualifier for modifiability. + * + *

Use on methods that preserve or transfer modifiability — for example, {@code List.subList()}, + * {@code iterator()}, or {@code stream()}. + * + *

The same qualifier that appears on the receiver will also appear on the return type (and + * possibly parameters). For example: + * + *

+ *  class Example {
+ *    @PolyModifiable List subList(@PolyModifiable List this, int from, int to);
+ *  }
+ * 
+ * + * If the receiver is {@code @Unmodifiable}, the return is {@code @Unmodifiable}. If the receiver is + * {@code @Modifiable}, the return is {@code @Modifiable}. + */ +@PolymorphicQualifier(UnknownModifiability.class) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface PolyModifiable {} diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java new file mode 100644 index 000000000000..1b5b3f1c8361 --- /dev/null +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -0,0 +1,27 @@ +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +public class PolyModifiableTest { + // A simple polymorphic method + @PolyModifiable + Object identity(@PolyModifiable Object x) { + return x; + } + + void testPoly( + @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object any) { + + // Allowed: preserving same modifiability + @Modifiable Object a = identity(mod); + @Unmodifiable Object b = identity(unmod); + @UnknownModifiability Object c = identity(any); + + // :: error: (assignment) + @Modifiable Object d = identity(unmod); // cannot upcast from Unmodifiable → Modifiable + + // :: error: (assignment) + @Unmodifiable Object e = identity(mod); // cannot downcast from Modifiable → Unmodifiable + } +} From 0efc9eb3b4839b6878da80532f9f6c3303c407f2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 12 Nov 2025 17:10:31 -0800 Subject: [PATCH 009/308] fixed hierarchy test --- .../tests/modifiability/HierarchyTest.java | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index 6fe9ee014568..59bf6f58c2aa 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -3,34 +3,58 @@ import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +/** + * Tests the subtyping relationships among the modifiability qualifiers: @UnknownModifiability (top) + * / \ @Modifiable @Unmodifiable \ / @BottomModifiable (bottom) + */ class HierarchyTest { void testHierarchy( - @UnknownModifiability Object any, @Modifiable Object mod, @Unmodifiable Object unmod) { + @UnknownModifiability Object any, + @Modifiable Object mod, + @Unmodifiable Object unmod, + @BottomModifiable Object b) { - // --- Assignments to UnknownModifiability (top) --- - @UnknownModifiability Object a1 = any; - @UnknownModifiability Object a2 = mod; - @UnknownModifiability Object a3 = unmod; // OK: both subtypes of UnknownModifiability + // ============================================================ + // Assignments to @UnknownModifiability (top) + // ============================================================ + + @UnknownModifiability Object a1 = any; // OK + @UnknownModifiability Object a2 = mod; // OK: Modifiable <: Unknown + @UnknownModifiability Object a3 = unmod; // OK: Unmodifiable <: Unknown + @UnknownModifiability Object a4 = b; // OK: Bottom <: Unknown + // ============================================================ + // Assignments to @Modifiable + // ============================================================ - // --- Assignments to Modifiable --- @Modifiable Object m1 = mod; // OK // :: error: (assignment) - @Modifiable Object m2 = any; // error: UnknownModifiability → Modifiable not allowed + @Modifiable Object m2 = any; // Error: Unknown → Modifiable not allowed // :: error: (assignment) - @Modifiable Object m3 = unmod; // error: Unmodifiable → Modifiable not allowed + @Modifiable Object m3 = unmod; // Error: Unmodifiable → Modifiable not allowed + @Modifiable Object m4 = b; // OK: Bottom <: Modifiable + + // ============================================================ + // Assignments to @Unmodifiable + // ============================================================ - // --- Assignments to Unmodifiable --- @Unmodifiable Object u1 = unmod; // OK // :: error: (assignment) - @Unmodifiable Object u2 = any; // error: UnknownModifiability → Unmodifiable not allowed + @Unmodifiable Object u2 = any; // Error: Unknown → Unmodifiable not allowed // :: error: (assignment) - @Unmodifiable Object u3 = mod; // error: Modifiable → Unmodifiable not allowed + @Unmodifiable Object u3 = mod; // Error: Modifiable → Unmodifiable not allowed + @Unmodifiable Object u4 = b; // OK: Bottom <: Unmodifiable + + // ============================================================ + // Assignments to @BottomModifiable (bottom) + // ============================================================ - // --- Reflexivity (bottom always OK) --- - @BottomModifiable Object b = null; - @Modifiable Object m4 = b; // OK: BottomModifiable <: Modifiable - @Unmodifiable Object u4 = b; // OK: BottomModifiable <: Unmodifiable - @UnknownModifiability Object a4 = b; // OK: BottomModifiable <: UnknownModifiability + @BottomModifiable Object b1 = b; // OK + // :: error: (assignment) + @BottomModifiable Object b2 = any; // Error: Unknown → Bottom not allowed + // :: error: (assignment) + @BottomModifiable Object b3 = mod; // Error: Modifiable → Bottom not allowed + // :: error: (assignment) + @BottomModifiable Object b4 = unmod; // Error: Unmodifiable → Bottom not allowed } } From e62662ca7b036fba7c3e558c8bd5dc0266af40cf Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 12 Nov 2025 23:56:45 -0800 Subject: [PATCH 010/308] add more test for polyModifiable --- .../modifiability/PolyModifiableTest.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 1b5b3f1c8361..85a3b27a8296 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,3 +1,4 @@ +import org.checkerframework.checker.modifiability.qual.BottomModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; @@ -11,17 +12,35 @@ Object identity(@PolyModifiable Object x) { } void testPoly( - @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object any) { + @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object any, @BottomModifiable Object bottom) { // Allowed: preserving same modifiability @Modifiable Object a = identity(mod); @Unmodifiable Object b = identity(unmod); @UnknownModifiability Object c = identity(any); + @BottomModifiable Object d = identity(bottom); // :: error: (assignment) - @Modifiable Object d = identity(unmod); // cannot upcast from Unmodifiable → Modifiable + @Modifiable Object e = identity(unmod); // cannot upcast from Unmodifiable → Modifiable + // :: error: (assignment) + @Modifiable Object f = identity(any); // cannot upcast from Unknown → Modifiable + @Modifiable Object h = identity(bottom); // ok to assign Bottom → Modifiable + + // :: error: (assignment) + @Unmodifiable Object i = identity(mod); // cannot downcast from Modifiable → Unmodifiable + // :: error: (assignment) + @Unmodifiable Object j = identity(any); // cannot downcast from Unknown → Unmodifiable + @Unmodifiable Object l = identity(bottom); // ok to assign Bottom → Unmodifiable + @UnknownModifiability Object m = identity(mod); // ok to assign Modifiable → Unknown + @UnknownModifiability Object n = identity(unmod); // ok to assign Unmodifiable → Unknown + @UnknownModifiability Object o = identity(bottom); // ok to assign Bottom → Unknown + + // :: error: (assignment) + @BottomModifiable Object p = identity(mod); // cannot upcast from Modifiable → Bottom + // :: error: (assignment) + @BottomModifiable Object q = identity(unmod); // cannot upcast from Unmodifiable → Bottom // :: error: (assignment) - @Unmodifiable Object e = identity(mod); // cannot downcast from Modifiable → Unmodifiable + @BottomModifiable Object r = identity(any); // cannot upcast from Unknown → Bottom } } From 963ca9d02c209670152be44b7a483751d43ba7e0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 12 Nov 2025 23:57:07 -0800 Subject: [PATCH 011/308] fixed formating --- checker/tests/modifiability/PolyModifiableTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 85a3b27a8296..0a5980d3ad86 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -12,7 +12,10 @@ Object identity(@PolyModifiable Object x) { } void testPoly( - @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object any, @BottomModifiable Object bottom) { + @Modifiable Object mod, + @Unmodifiable Object unmod, + @UnknownModifiability Object any, + @BottomModifiable Object bottom) { // Allowed: preserving same modifiability @Modifiable Object a = identity(mod); @@ -41,6 +44,6 @@ void testPoly( // :: error: (assignment) @BottomModifiable Object q = identity(unmod); // cannot upcast from Unmodifiable → Bottom // :: error: (assignment) - @BottomModifiable Object r = identity(any); // cannot upcast from Unknown → Bottom + @BottomModifiable Object r = identity(any); // cannot upcast from Unknown → Bottom } } From 8a8961f39ca077e066f59c744ca87439b2d9fbe1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 13 Nov 2025 14:38:56 -0800 Subject: [PATCH 012/308] add ModifiabilityChecker --- .../modifiability/ModifiabilityChecker.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java new file mode 100644 index 000000000000..af098d84faa4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability; + +import java.util.Collection; +import java.util.Iterator; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * A type-checker that warns, at compile time, if a program might throw {@link + * UnsupportedOperationException} at run time due to calling a mutating method on an unmodifiable + * collection. + * + *

The checker enforces the Modifiability type system, where {@code @Modifiable} collections can + * be safely mutated and {@code @Unmodifiable} collections cannot be mutated without risking an + * {@link UnsupportedOperationException}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({Collection.class, Iterator.class}) +@SuppressWarningsPrefix({"modifiable", "unmodifiable"}) +public class ModifiabilityChecker extends BaseTypeChecker { + /** Creates a Modifiability checker. */ + public ModifiabilityChecker() {} +} From 211a18d8ab0be5857a925d632e939e9e3e646621 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 17 Nov 2025 12:56:59 -0800 Subject: [PATCH 013/308] add ModifiabiityTest --- .../checker/test/junit/ModifiabilityTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 checker/src/test/java/org/checkerframework/checker/test/junit/ModifiabilityTest.java diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ModifiabilityTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ModifiabilityTest.java new file mode 100644 index 000000000000..3ba5c6a8c81e --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ModifiabilityTest.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.checker.modifiability.ModifiabilityChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +public class ModifiabilityTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a ModifiabilityTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public ModifiabilityTest(List testFiles) { + super(testFiles, ModifiabilityChecker.class, "modifiability", "-Anomsgtext"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"modifiability", "all-systems"}; + } +} From 704b3346d8f2f9f03ad4d06450573748d1ee4208 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 23 Nov 2025 18:35:16 -0800 Subject: [PATCH 014/308] tested the partially annotated List.java in JDK --- .../modifiability/qual/PolyModifiable.java | 8 +- .../modifiability/BasicModifiabilityTest.java | 132 ++++++++++++++++++ .../modifiability/BasicModifiableTest3 2.java | 22 +++ 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 checker/tests/modifiability/BasicModifiabilityTest.java create mode 100644 checker/tests/modifiability/BasicModifiableTest3 2.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index afdb2fd582d0..fa00a5322dee 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -13,14 +13,12 @@ *

The same qualifier that appears on the receiver will also appear on the return type (and * possibly parameters). For example: * - *

+ * 

  *  class Example {
  *    @PolyModifiable List subList(@PolyModifiable List this, int from, int to);
  *  }
- * 
- * - * If the receiver is {@code @Unmodifiable}, the return is {@code @Unmodifiable}. If the receiver is - * {@code @Modifiable}, the return is {@code @Modifiable}. + *
If the receiver is {@code @Unmodifiable}, the return is {@code @Unmodifiable}. If + * the receiver is {@code @Modifiable}, the return is {@code @Modifiable}. */ @PolymorphicQualifier(UnknownModifiability.class) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java new file mode 100644 index 000000000000..b29cab384977 --- /dev/null +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -0,0 +1,132 @@ +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +// this checkes the correctness of annotation in List.java and ONLY the initialization of ArrayList +public class BasicModifiabilityTest { + + void testBasicModifiable() { + // Modifiable collections should allow mutation + List modifiableList = new ArrayList<>(); + modifiableList.add("test"); + modifiableList.remove(0); + + // Unmodifiable collections should not allow mutation + @Unmodifiable List unmodifiableList = List.of("test1", "test2"); + // :: error: (method.invocation) + unmodifiableList.add("test3"); + // :: error: (method.invocation) + unmodifiableList.remove(0); + // :: error: (method.invocation) + unmodifiableList.remove("test1"); + } + + void testUnmodifiableFactoryMethods() { + // These should be inferred as @Unmodifiable + List list1 = List.of("a", "b"); + // :: error: (method.invocation) + list1.add("c"); + + // Explicit unmodifiable type + unmodifiable factory + @Unmodifiable List list1u = List.of("x", "y"); + + // :: error: (assignment) + @Modifiable List cannotBeMod1 = List.of("m1", "m2"); + + List list2 = List.copyOf(new ArrayList<>()); + // :: error: (method.invocation) + list2.add("c"); + // :: error: (method.invocation) + list2.remove(0); + + List src = new ArrayList<>(); + src.add("s"); + + @Unmodifiable List list2u = List.copyOf(src); + // :: error: (assignment) + @Modifiable List cannotBeMod2 = List.copyOf(src); + } + + void testModifiableFactoryMethods() { + // This should be inferred as @Modifiable + List modifiableList = new ArrayList<>(); + modifiableList.add("test"); + modifiableList.remove(0); + } + + void testToArrayModifiability( + @Modifiable List modList, + @Unmodifiable List unmodList, + @UnknownModifiability List anyList) { + + // toArray is annotated to return @Modifiable Object[] + @Modifiable Object[] arr1 = modList.toArray(); + @Modifiable Object[] arr2 = unmodList.toArray(); + + @Modifiable String[] fromMod = modList.toArray(new String[0]); + @Modifiable String[] fromUnmod = unmodList.toArray(new String[0]); + @Modifiable String[] fromAny = anyList.toArray(new String[0]); + + // It should be an error to treat the returned array as @Unmodifiable. + // :: error: (assignment) + @Unmodifiable Object[] bad1 = modList.toArray(); + // :: error: (assignment) + @Unmodifiable Object[] bad2 = unmodList.toArray(); + // :: error: (assignment) + @Unmodifiable String[] u1 = modList.toArray(new String[0]); + // :: error: (assignment) + @Unmodifiable String[] u2 = unmodList.toArray(new String[0]); + + // But assigning to @UnknownModifiability is OK (Unknown is the top type). + @UnknownModifiability String[] anyArr = modList.toArray(new String[0]); + } + + void testToArrayArgumentModifiability( + List list, + @Modifiable String[] modArr, + @Unmodifiable String[] unmodArr, + @UnknownModifiability String[] anyArr) { + + // Argument array modifiability should not matter: parameter is T[] with + // default @UnknownModifiability on the array object. + + list.toArray(modArr); + list.toArray(unmodArr); + list.toArray(anyArr); + } + + void testIteratorPolymorphic( + @Modifiable List modList, @Unmodifiable List unmodList) { + + // iterator() is annotated with @PolyModifiable on both receiver and return type. + // So the iterator from a modifiable list is modifiable: + @Modifiable Iterator itMod = modList.iterator(); + + // And the iterator from an unmodifiable list is unmodifiable: + @Unmodifiable Iterator itUnmod = unmodList.iterator(); + + // Trying to treat the unmodifiable iterator as modifiable should be rejected. + // :: error: (assignment) + @Modifiable Iterator badIt = unmodList.iterator(); + } + + void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List unmod) { + List other = new ArrayList<>(); + other.add("x"); + + // OK on modifiable + mod.addAll(other); + mod.removeAll(other); + mod.retainAll(other); + + // :: error: (method.invocation) + unmod.addAll(other); + // :: error: (method.invocation) + unmod.removeAll(other); + // :: error: (method.invocation) + unmod.retainAll(other); + } +} diff --git a/checker/tests/modifiability/BasicModifiableTest3 2.java b/checker/tests/modifiability/BasicModifiableTest3 2.java new file mode 100644 index 000000000000..1134542a6e3d --- /dev/null +++ b/checker/tests/modifiability/BasicModifiableTest3 2.java @@ -0,0 +1,22 @@ +import java.util.List; +import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +public class BasicModifiableTest3 { + + /* + @ReleasesNoLocks + @SideEffectsOnly("this") + @EnsuresNonEmpty("this") + */ + boolean my_add(@GuardSatisfied List this_one, E e) { + return true; + } + + void testBasicModifiable() { + // Unmodifiable collections should not allow mutation + @Unmodifiable List unmodifiableList = List.of("test1", "test2"); + // :: error: (method.invocation) + my_add(unmodifiableList, "test3"); + } +} From 562aa0e73857faef39fd52090eb10a0a0bfb727d Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 25 Nov 2025 12:27:57 -0800 Subject: [PATCH 015/308] fixed the test for list.java --- .../modifiability/BasicModifiabilityTest.java | 55 +++++++------------ ...Test3 2.java => BasicModifiableTest3.java} | 5 +- 2 files changed, 24 insertions(+), 36 deletions(-) rename checker/tests/modifiability/{BasicModifiableTest3 2.java => BasicModifiableTest3.java} (75%) diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index b29cab384977..dd26d10145c0 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -62,40 +62,13 @@ void testToArrayModifiability( @Unmodifiable List unmodList, @UnknownModifiability List anyList) { - // toArray is annotated to return @Modifiable Object[] - @Modifiable Object[] arr1 = modList.toArray(); - @Modifiable Object[] arr2 = unmodList.toArray(); + modList.toArray(); + unmodList.toArray(); + anyList.toArray(); - @Modifiable String[] fromMod = modList.toArray(new String[0]); - @Modifiable String[] fromUnmod = unmodList.toArray(new String[0]); - @Modifiable String[] fromAny = anyList.toArray(new String[0]); - - // It should be an error to treat the returned array as @Unmodifiable. - // :: error: (assignment) - @Unmodifiable Object[] bad1 = modList.toArray(); - // :: error: (assignment) - @Unmodifiable Object[] bad2 = unmodList.toArray(); - // :: error: (assignment) - @Unmodifiable String[] u1 = modList.toArray(new String[0]); - // :: error: (assignment) - @Unmodifiable String[] u2 = unmodList.toArray(new String[0]); - - // But assigning to @UnknownModifiability is OK (Unknown is the top type). - @UnknownModifiability String[] anyArr = modList.toArray(new String[0]); - } - - void testToArrayArgumentModifiability( - List list, - @Modifiable String[] modArr, - @Unmodifiable String[] unmodArr, - @UnknownModifiability String[] anyArr) { - - // Argument array modifiability should not matter: parameter is T[] with - // default @UnknownModifiability on the array object. - - list.toArray(modArr); - list.toArray(unmodArr); - list.toArray(anyArr); + String[] fromMod = modList.toArray(new String[0]); + String[] fromUnmod = unmodList.toArray(new String[0]); + String[] fromAny = anyList.toArray(new String[0]); } void testIteratorPolymorphic( @@ -117,7 +90,6 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List List other = new ArrayList<>(); other.add("x"); - // OK on modifiable mod.addAll(other); mod.removeAll(other); mod.retainAll(other); @@ -129,4 +101,19 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List // :: error: (method.invocation) unmod.retainAll(other); } + + void testReplaceAll(@Modifiable List mod, @Unmodifiable List unmod) { + mod.replaceAll(s -> s + "!"); + + // :: error: (method.invocation) + unmod.replaceAll(s -> s + "!"); + } + + void testSubList(@Modifiable List mod, @Unmodifiable List unmod) { + List sub = mod.subList(0, mod.size()); + sub.clear(); // OK because sub is modifiable + + // :: error: (assignment) + @Modifiable List sub2 = unmod.subList(0, unmod.size()); + } } diff --git a/checker/tests/modifiability/BasicModifiableTest3 2.java b/checker/tests/modifiability/BasicModifiableTest3.java similarity index 75% rename from checker/tests/modifiability/BasicModifiableTest3 2.java rename to checker/tests/modifiability/BasicModifiableTest3.java index 1134542a6e3d..b1a02e1ee370 100644 --- a/checker/tests/modifiability/BasicModifiableTest3 2.java +++ b/checker/tests/modifiability/BasicModifiableTest3.java @@ -1,5 +1,6 @@ import java.util.List; import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; public class BasicModifiableTest3 { @@ -9,14 +10,14 @@ public class BasicModifiableTest3 { @SideEffectsOnly("this") @EnsuresNonEmpty("this") */ - boolean my_add(@GuardSatisfied List this_one, E e) { + boolean my_add(@Modifiable @GuardSatisfied List this_one, E e) { return true; } void testBasicModifiable() { // Unmodifiable collections should not allow mutation @Unmodifiable List unmodifiableList = List.of("test1", "test2"); - // :: error: (method.invocation) + // :: error: (argument) my_add(unmodifiableList, "test3"); } } From e41e65cacc5a2f016959f4fa0f8fa4ae312f7a24 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Dec 2025 00:16:09 -0800 Subject: [PATCH 016/308] added map class to Modifiability Checker, added test for Map.java annotations. --- .../modifiability/ModifiabilityChecker.java | 3 +- .../modifiability/MapModifiableTest.java | 109 ++++++++++++++++++ .../tests/all-systems/DiamondLambda.java | 1 + 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 checker/tests/modifiability/MapModifiableTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index af098d84faa4..178a54571b27 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.Iterator; +import java.util.Map; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; @@ -17,7 +18,7 @@ * * @checker_framework.manual #modifiability-checker Modifiability Checker */ -@RelevantJavaTypes({Collection.class, Iterator.class}) +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) @SuppressWarningsPrefix({"modifiable", "unmodifiable"}) public class ModifiabilityChecker extends BaseTypeChecker { /** Creates a Modifiability checker. */ diff --git a/checker/tests/modifiability/MapModifiableTest.java b/checker/tests/modifiability/MapModifiableTest.java new file mode 100644 index 000000000000..2d7320cd19e4 --- /dev/null +++ b/checker/tests/modifiability/MapModifiableTest.java @@ -0,0 +1,109 @@ +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +// Tests for Map mutating and read-only methods with modifiability qualifiers. +public class MapModifiableTest { + + void testBasicMutatingOperations() { + @Modifiable Map mod = new HashMap<>(); + mod.put("a", 1); + mod.putIfAbsent("b", 2); + mod.putAll(Map.of("c", 3)); + mod.remove("a"); + mod.remove("b", 2); + mod.replace("c", 4); + mod.replace("c", 4, 5); + mod.replaceAll((k, v) -> v + 1); + mod.compute("d", (k, v) -> (v == null) ? 1 : v + 1); + mod.computeIfAbsent("e", k -> 7); + mod.computeIfPresent("e", (k, v) -> v + 1); + mod.merge("f", 1, (oldV, newV) -> oldV + newV); + mod.clear(); + + @Unmodifiable Map unmod = Map.of("x", 1); + // :: error: (method.invocation) + unmod.put("y", 2); + // :: error: (method.invocation) + unmod.putIfAbsent("z", 3); + // :: error: (method.invocation) + unmod.putAll(Map.of("a", 1)); + // :: error: (method.invocation) + unmod.remove("x"); + // :: error: (method.invocation) + unmod.remove("x", 1); + // :: error: (method.invocation) + unmod.replace("x", 2); + // :: error: (method.invocation) + unmod.replace("x", 2, 3); + // :: error: (method.invocation) + unmod.replaceAll((k, v) -> v + 1); + // :: error: (method.invocation) + unmod.compute("g", (k, v) -> 1); + // :: error: (method.invocation) + unmod.computeIfAbsent("h", k -> 2); + // :: error: (method.invocation) + unmod.computeIfPresent("h", (k, v) -> v + 1); + // :: error: (method.invocation) + unmod.merge("i", 1, (oldV, newV) -> oldV + newV); + // :: error: (method.invocation) + unmod.clear(); + } + + void testReadOnlyOperations() { + Map map = Map.of("a", 1); + map.get("a"); + map.getOrDefault("b", 0); + map.containsKey("a"); + map.containsValue(1); + map.isEmpty(); + map.size(); + map.hashCode(); + map.equals(Map.of("a", 1)); + Set> entries = map.entrySet(); + Collection vals = map.values(); + Set keys = map.keySet(); + map.forEach((k, v) -> {}); + } + + void testFactoryMethodsAndAssignments() { + // Map.of and Map.copyOf are unmodifiable. + Map inferred = Map.of("k", 1); + @Unmodifiable Map explicitUnmod = Map.of("k", 1); + + // :: error: (assignment) + @Modifiable Map cannotBeMod1 = Map.of("m1", 1); + + Map src = new HashMap<>(); + src.put("s", 1); + Map copy = Map.copyOf(src); + // :: error: (assignment) + @Modifiable Map cannotBeMod2 = Map.copyOf(src); + } + + void testEntryModifiabilityLocals(@Modifiable Entry modEntry) { + modEntry.setValue(2); + + @Unmodifiable Entry immEntry = Map.entry("k", 1); + // :: error: (method.invocation) + immEntry.setValue(5); + } + + void testAnnotatedLocalsFromParams( + @UnknownModifiability Map any, + @Modifiable Map modSrc, + @Unmodifiable Map unmodSrc) { + + modSrc.put("p", 1); + // :: error: (method.invocation) + unmodSrc.put("p", 1); + // depending on defaults, this may be an error: + // :: error: (method.invocation) + any.put("q", 2); + } +} diff --git a/framework/tests/all-systems/DiamondLambda.java b/framework/tests/all-systems/DiamondLambda.java index e66dfceb9104..5366f92796e5 100644 --- a/framework/tests/all-systems/DiamondLambda.java +++ b/framework/tests/all-systems/DiamondLambda.java @@ -4,6 +4,7 @@ public class DiamondLambda { void foo(Map> map, String sequence) { + // :: error: (method.invocation) Set set = map.computeIfAbsent(sequence, __ -> new HashSet<>()); } } From 749b84a07bc0d54b95e59d0803bc9f3bf539898f Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Dec 2025 00:44:42 -0800 Subject: [PATCH 017/308] add test for collection.java --- .../CollectionModifiableTest.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 checker/tests/modifiability/CollectionModifiableTest.java diff --git a/checker/tests/modifiability/CollectionModifiableTest.java b/checker/tests/modifiability/CollectionModifiableTest.java new file mode 100644 index 000000000000..d08b337064f1 --- /dev/null +++ b/checker/tests/modifiability/CollectionModifiableTest.java @@ -0,0 +1,83 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Spliterator; +import java.util.stream.Stream; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +// Tests for Collection mutating and read-only methods with modifiability qualifiers. +public class CollectionModifiableTest { + + void testMutatingMethods() { + @Modifiable Collection mod = new ArrayList<>(); + mod.add("a"); + mod.addAll(List.of("b", "c")); + mod.clear(); + mod.remove("a"); + mod.removeAll(List.of("b")); + mod.retainAll(List.of("c")); + mod.removeIf(s -> s.isEmpty()); + + @Unmodifiable Collection unmod = List.of("x", "y"); + // :: error: (method.invocation) + unmod.add("z"); + // :: error: (method.invocation) + unmod.addAll(List.of("z")); + // :: error: (method.invocation) + unmod.clear(); + // :: error: (method.invocation) + unmod.remove("x"); + // :: error: (method.invocation) + unmod.removeAll(List.of("x")); + // :: error: (method.invocation) + unmod.retainAll(List.of("y")); + // :: error: (method.invocation) + unmod.removeIf(s -> true); + } + + void testReadOnlyMethods() { + Collection c = List.of("a", "b"); + boolean has = c.contains("a"); + boolean all = c.containsAll(List.of("a")); + boolean eq = c.equals(List.of("a", "b")); + int h = c.hashCode(); + boolean empty = c.isEmpty(); + int sz = c.size(); + Spliterator sp = c.spliterator(); + Stream st = c.stream(); + Stream pst = c.parallelStream(); + Object[] arr = c.toArray(); + String[] strs = c.toArray(new String[0]); + // iterate to exercise iterator() (non-mutating usage) + for (String s : c) {} + } + + void testFactoryAssignments() { + // List.of returns an unmodifiable collection view + Collection inferred = List.of("k"); + @Unmodifiable Collection explicitUnmod = List.of("k"); + + // :: error: (assignment) + @Modifiable Collection cannotBeMod1 = List.of("m1"); + + List src = new ArrayList<>(); + src.add("s"); + Collection copy = List.copyOf(src); + // :: error: (assignment) + @Modifiable Collection cannotBeMod2 = List.copyOf(src); + } + + void testAnnotatedLocalsFromParams( + @Modifiable Collection mod, + @Unmodifiable Collection unmod, + @UnknownModifiability Collection unknown) { + mod.add("p"); + // :: error: (method.invocation) + unmod.add("p"); + // depending on defaults, this may be an error: + // :: error: (method.invocation) + unknown.add("q"); + } +} From 7f7538a6e324c8732c3cbb308a9620c05690f12e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 3 Dec 2025 22:02:30 -0800 Subject: [PATCH 018/308] finished set.class annotation --- .../modifiability/SetModifiableTest.java | 61 +++++++++++++++++++ .../tests/all-systems/DiamondLambda.java | 4 +- framework/tests/all-systems/Issue6810.java | 4 +- 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 checker/tests/modifiability/SetModifiableTest.java diff --git a/checker/tests/modifiability/SetModifiableTest.java b/checker/tests/modifiability/SetModifiableTest.java new file mode 100644 index 000000000000..e9a118e4063a --- /dev/null +++ b/checker/tests/modifiability/SetModifiableTest.java @@ -0,0 +1,61 @@ +import java.util.HashSet; +import java.util.Set; +import java.util.Spliterator; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +// Tests for Set mutating and read-only methods with modifiability qualifiers. +public class SetModifiableTest { + + void testMutatingMethods() { + @Modifiable Set mod = new HashSet<>(); + mod.add("a"); + mod.addAll(Set.of("b", "c")); + mod.remove("a"); + mod.removeAll(Set.of("b")); + mod.retainAll(Set.of("c")); + mod.clear(); + + @Unmodifiable Set unmod = Set.of("x", "y"); + // :: error: (method.invocation) + unmod.add("z"); + // :: error: (method.invocation) + unmod.addAll(Set.of("z")); + // :: error: (method.invocation) + unmod.remove("x"); + // :: error: (method.invocation) + unmod.removeAll(Set.of("x")); + // :: error: (method.invocation) + unmod.retainAll(Set.of("y")); + // :: error: (method.invocation) + unmod.clear(); + } + + void testReadOnlyMethods() { + Set set = Set.of("a", "b"); + set.contains("a"); + set.containsAll(Set.of("a")); + set.equals(Set.of("a", "b")); + set.hashCode(); + set.isEmpty(); + set.size(); + Spliterator sp = set.spliterator(); + Object[] arr = set.toArray(); + String[] strs = set.toArray(new String[0]); + for (String s : set) {} + } + + void testFactoryAssignments() { + Set inferred = Set.of("k"); + @Unmodifiable Set explicitUnmod = Set.of("k"); + + // :: error: (assignment) + @Modifiable Set cannotBeMod1 = Set.of("m1"); + + Set src = new HashSet<>(); + src.add("s"); + Set copy = Set.copyOf(src); + // :: error: (assignment) + @Modifiable Set cannotBeMod2 = Set.copyOf(src); + } +} diff --git a/framework/tests/all-systems/DiamondLambda.java b/framework/tests/all-systems/DiamondLambda.java index 5366f92796e5..b364f5e0a1d5 100644 --- a/framework/tests/all-systems/DiamondLambda.java +++ b/framework/tests/all-systems/DiamondLambda.java @@ -1,10 +1,10 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Modifiable; public class DiamondLambda { - void foo(Map> map, String sequence) { - // :: error: (method.invocation) + void foo(@Modifiable Map> map, String sequence) { Set set = map.computeIfAbsent(sequence, __ -> new HashSet<>()); } } diff --git a/framework/tests/all-systems/Issue6810.java b/framework/tests/all-systems/Issue6810.java index 2f19cb676775..40a6b1c34389 100644 --- a/framework/tests/all-systems/Issue6810.java +++ b/framework/tests/all-systems/Issue6810.java @@ -1,11 +1,13 @@ import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Modifiable; public class Issue6810 { static class Box> {} abstract static class BoxSet> implements Set {} - static > void intersect2(BoxSet intersect, Set set2) { + static > void intersect2( + @Modifiable BoxSet intersect, Set set2) { intersect.retainAll(set2); } } From f43925c6a65566ac454ec13d352825e5c5431701 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 7 Dec 2025 02:23:49 -0800 Subject: [PATCH 019/308] finished and tested the annotations of Collections.java --- .../modifiability/ModifiabilityChecker.java | 9 +- .../CollectionsModifiableTest.java | 302 ++++++++++++++++++ .../modifiability/MapModifiableTest.java | 4 +- 3 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 checker/tests/modifiability/CollectionsModifiableTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 178a54571b27..df8d0100a434 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.modifiability; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.Map; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -18,7 +19,13 @@ * * @checker_framework.manual #modifiability-checker Modifiability Checker */ -@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) +@RelevantJavaTypes({ + Collection.class, + Iterator.class, + Map.class, + Map.Entry.class, + Collections.class +}) @SuppressWarningsPrefix({"modifiable", "unmodifiable"}) public class ModifiabilityChecker extends BaseTypeChecker { /** Creates a Modifiability checker. */ diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java new file mode 100644 index 000000000000..4a88da7086a4 --- /dev/null +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -0,0 +1,302 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +public class CollectionsModifiableTest { + + void testAddAll() { + @Modifiable Collection mod = new ArrayList<>(); + Collections.addAll(mod, "a", "b"); + + @Unmodifiable Collection unmod = Collections.unmodifiableCollection(mod); + // :: error: (argument) + Collections.addAll(unmod, "c"); + } + + void testAsLifoQueue() { + @Modifiable Deque mod = new LinkedList<>(); + Queue q = Collections.asLifoQueue(mod); + q.add("a"); + + // there is no unmodifiable deque. + } + + void testBinarySearch() { + @Modifiable List mod = new ArrayList<>(); + mod.add("a"); + Collections.binarySearch(mod, "a"); + + @Unmodifiable List unmod = Collections.unmodifiableList(mod); + Collections.binarySearch(unmod, "a"); + } + + void testCheckedWrappers() { + @Modifiable Collection c = new ArrayList<>(); + @Modifiable Collection checkedC = Collections.checkedCollection(c, String.class); + checkedC.add("a"); + + @Modifiable List l = new ArrayList<>(); + @Modifiable List checkedL = Collections.checkedList(l, String.class); + checkedL.add("a"); + + @Modifiable Map m = new HashMap<>(); + @Modifiable + Map checkedM = Collections.checkedMap(m, String.class, String.class); + checkedM.put("a", "b"); + + @Modifiable Set s = new HashSet<>(); + @Modifiable Set checkedS = Collections.checkedSet(s, String.class); + checkedS.add("a"); + + // Checked wrappers should return modifiable views if the underlying collection is modifiable + // But if we pass an unmodifiable collection, it should probably be an error or return + // unmodifiable? + // The JDK implementation just wraps it. The checker should probably enforce that the input is + // modifiable if the output is used as modifiable. + // However, checked wrappers are usually used to add runtime type safety to a modifiable + // collection. + } + + void testCopy() { + @Modifiable List dest = new ArrayList<>(); + dest.add("a"); + List src = new ArrayList<>(); + src.add("b"); + + Collections.copy(dest, src); + + @Unmodifiable List unmodDest = Collections.unmodifiableList(dest); + // :: error: (argument) + Collections.copy(unmodDest, src); + } + + void testDisjoint() { + Collection c1 = new ArrayList<>(); + Collection c2 = new ArrayList<>(); + Collections.disjoint(c1, c2); + } + + void testEmptyCollections() { + @Unmodifiable List l = Collections.emptyList(); + // :: error: (method.invocation) + l.add("a"); + // :: error: (assignment) + @Modifiable List modL = Collections.emptyList(); + + @Unmodifiable Set s = Collections.emptySet(); + // :: error: (method.invocation) + s.add("a"); + // :: error: (assignment) + @Modifiable Set modS = Collections.emptySet(); + + @Unmodifiable Map m = Collections.emptyMap(); + // :: error: (method.invocation) + m.put("a", "b"); + // :: error: (assignment) + @Modifiable Map modM = Collections.emptyMap(); + + @Unmodifiable Iterator it = Collections.emptyIterator(); + // Iterator.remove() is a mutating method + // :: error: (method.invocation) + it.remove(); + } + + void testFill() { + @Modifiable List list = new ArrayList<>(); + list.add("a"); + Collections.fill(list, "b"); + + @Unmodifiable List unmod = Collections.unmodifiableList(list); + // :: error: (argument) + Collections.fill(unmod, "c"); + } + + void testFrequency() { + Collection c = new ArrayList<>(); + Collections.frequency(c, "a"); + } + + void testIndexOfSubList() { + List src = new ArrayList<>(); + List target = new ArrayList<>(); + Collections.indexOfSubList(src, target); + Collections.lastIndexOfSubList(src, target); + } + + void testListFromEnumeration() { + Enumeration e = Collections.emptyEnumeration(); + // Returns ArrayList which is modifiable + @Modifiable ArrayList l = Collections.list(e); + l.add("a"); + } + + void testMinMax() { + Collection c = new ArrayList<>(); + Collections.max(c); + Collections.min(c); + } + + void testNCopies() { + @Unmodifiable List l = Collections.nCopies(5, "a"); + // :: error: (method.invocation) + l.add("b"); + // :: error: (assignment) + @Modifiable List mod = Collections.nCopies(5, "a"); + } + + void testReplaceAll() { + @Modifiable List list = new ArrayList<>(); + list.add("a"); + Collections.replaceAll(list, "a", "b"); + + @Unmodifiable List unmod = Collections.unmodifiableList(list); + // :: error: (argument) + Collections.replaceAll(unmod, "a", "b"); + } + + void testReverse() { + @Modifiable List list = new ArrayList<>(); + list.add("a"); + Collections.reverse(list); + + @Unmodifiable List unmod = Collections.unmodifiableList(list); + // :: error: (argument) + Collections.reverse(unmod); + } + + void testRotate() { + @Modifiable List list = new ArrayList<>(); + list.add("a"); + Collections.rotate(list, 1); + + @Unmodifiable List unmod = Collections.unmodifiableList(list); + // :: error: (argument) + Collections.rotate(unmod, 1); + } + + void testShuffle() { + @Modifiable List list = new ArrayList<>(); + list.add("a"); + Collections.shuffle(list); + Collections.shuffle(list, new Random()); + + @Unmodifiable List unmod = Collections.unmodifiableList(list); + // :: error: (argument) + Collections.shuffle(unmod); + // :: error: (argument) + Collections.shuffle(unmod, new Random()); + } + + void testSingleton() { + @Unmodifiable Set s = Collections.singleton("a"); + // :: error: (method.invocation) + s.add("b"); + // :: error: (assignment) + @Modifiable Set modS = Collections.singleton("a"); + + @Unmodifiable List l = Collections.singletonList("a"); + // :: error: (method.invocation) + l.add("b"); + // :: error: (assignment) + @Modifiable List modL = Collections.singletonList("a"); + + @Unmodifiable Map m = Collections.singletonMap("a", "b"); + // :: error: (method.invocation) + m.put("c", "d"); + // :: error: (assignment) + @Modifiable Map modM = Collections.singletonMap("a", "b"); + } + + void testSort() { + @Modifiable List list = new ArrayList<>(); + list.add("a"); + Collections.sort(list); + Collections.sort(list, Comparator.naturalOrder()); + + @Unmodifiable List unmod = Collections.unmodifiableList(list); + // :: error: (argument) + Collections.sort(unmod); + // :: error: (argument) + Collections.sort(unmod, Comparator.naturalOrder()); + } + + void testSwap() { + @Modifiable List mod = new ArrayList<>(); + mod.add("a"); + mod.add("b"); + Collections.swap(mod, 0, 1); + + @Unmodifiable List unmod = Collections.unmodifiableList(mod); + // :: error: (argument) + Collections.swap(unmod, 0, 1); + } + + void testSynchronizedWrappers() { + // Synchronized wrappers pass through modifiability + @Modifiable Collection c = new ArrayList<>(); + @Modifiable Collection syncC = Collections.synchronizedCollection(c); + syncC.add("a"); + + @Modifiable List l = new ArrayList<>(); + @Modifiable List syncL = Collections.synchronizedList(l); + syncL.add("a"); + } + + void testUnmodifiableWrappers() { + @Modifiable Collection c = new ArrayList<>(); + @Unmodifiable Collection unmodC = Collections.unmodifiableCollection(c); + // :: error: (method.invocation) + unmodC.add("a"); + // :: error: (assignment) + @Modifiable Collection modC = Collections.unmodifiableCollection(c); + + @Modifiable List l = new ArrayList<>(); + @Unmodifiable List unmodL = Collections.unmodifiableList(l); + // :: error: (method.invocation) + unmodL.add("a"); + // :: error: (assignment) + @Modifiable List modL = Collections.unmodifiableList(l); + + @Modifiable Set s = new HashSet<>(); + @Unmodifiable Set unmodS = Collections.unmodifiableSet(s); + // :: error: (method.invocation) + unmodS.add("a"); + // :: error: (assignment) + @Modifiable Set modS = Collections.unmodifiableSet(s); + + @Modifiable Map m = new HashMap<>(); + @Unmodifiable Map unmodM = Collections.unmodifiableMap(m); + // :: error: (method.invocation) + unmodM.put("a", "b"); + // :: error: (assignment) + @Modifiable Map modM = Collections.unmodifiableMap(m); + + @Modifiable SortedSet ss = new TreeSet<>(); + @Unmodifiable SortedSet unmodSS = Collections.unmodifiableSortedSet(ss); + // :: error: (method.invocation) + unmodSS.add("a"); + + @Modifiable SortedMap sm = new TreeMap<>(); + @Unmodifiable SortedMap unmodSM = Collections.unmodifiableSortedMap(sm); + // :: error: (method.invocation) + unmodSM.put("a", "b"); + } +} diff --git a/checker/tests/modifiability/MapModifiableTest.java b/checker/tests/modifiability/MapModifiableTest.java index 2d7320cd19e4..601511066670 100644 --- a/checker/tests/modifiability/MapModifiableTest.java +++ b/checker/tests/modifiability/MapModifiableTest.java @@ -56,7 +56,7 @@ void testBasicMutatingOperations() { } void testReadOnlyOperations() { - Map map = Map.of("a", 1); + @Unmodifiable Map map = Map.of("a", 1); map.get("a"); map.getOrDefault("b", 0); map.containsKey("a"); @@ -65,7 +65,7 @@ void testReadOnlyOperations() { map.size(); map.hashCode(); map.equals(Map.of("a", 1)); - Set> entries = map.entrySet(); + @Unmodifiable Set<@Unmodifiable Entry> entries = map.entrySet(); Collection vals = map.values(); Set keys = map.keySet(); map.forEach((k, v) -> {}); From c03ab20e96cd5535281316e324275d92a1d3120c Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 12 Jan 2026 00:16:47 -0800 Subject: [PATCH 020/308] added type rules for iterators --- .../ModifiabilityAnnotatedTypeFactory.java | 56 +++++++++++++++++++ .../modifiability/BasicModifiabilityTest.java | 4 +- 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java new file mode 100644 index 000000000000..6ddbe968e69c --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -0,0 +1,56 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.ExpressionTree; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TreeUtils; + +public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + private final ExecutableElement iteratorMethodElement; + private final AnnotationMirror UNMODIFIABLE; + private final AnnotationMirror UNKNOWN_MODIFIABILITY; + + @SuppressWarnings("this-escape") + public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.iteratorMethodElement = + TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); + this.UNMODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Unmodifiable.class); + this.UNKNOWN_MODIFIABILITY = + AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); + postInit(); + } + + @Override + public ParameterizedExecutableType methodFromUse( + ExpressionTree tree, + ExecutableElement methodElt, + AnnotatedTypeMirror receiverType, + boolean inferTypeArgs) { + + ParameterizedExecutableType mType = + super.methodFromUse(tree, methodElt, receiverType, inferTypeArgs); + + if (TreeUtils.isMethodInvocation(tree, iteratorMethodElement, processingEnv)) { + + AnnotatedTypeMirror returnType = mType.executableType.getReturnType(); + + if (receiverType.hasPrimaryAnnotation(Unmodifiable.class)) { + returnType.replaceAnnotation(UNMODIFIABLE); + } else if (receiverType.hasPrimaryAnnotation(Modifiable.class)) { + returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); + } + } + + return mType; + } +} diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index dd26d10145c0..3a4bbda81106 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -74,9 +74,7 @@ void testToArrayModifiability( void testIteratorPolymorphic( @Modifiable List modList, @Unmodifiable List unmodList) { - // iterator() is annotated with @PolyModifiable on both receiver and return type. - // So the iterator from a modifiable list is modifiable: - @Modifiable Iterator itMod = modList.iterator(); + @UnknownModifiability Iterator itMod = modList.iterator(); // And the iterator from an unmodifiable list is unmodifiable: @Unmodifiable Iterator itUnmod = unmodList.iterator(); From 5afc8f3322e91c2bdf45e1f6c3d782fe167e14b2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 18 Jan 2026 15:38:58 -0800 Subject: [PATCH 021/308] fixed empty iterator; added annotations in all-system; added type visitor, suppress the "constructor result must be TOP" check. --- .../modifiability/ModifiabilityVisitor.java | 33 +++++++++++++++++++ .../CollectionsModifiableTest.java | 5 +-- .../tests/all-systems/GenericsCasts.java | 2 ++ .../tests/all-systems/GenericsEnclosing.java | 3 +- .../tests/all-systems/RawTypeAssignment.java | 2 ++ .../all-systems/java8inference/Issue3036.java | 3 +- 6 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java new file mode 100644 index 000000000000..9fe58a39f063 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -0,0 +1,33 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.MethodInvocationTree; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.qual.WillThrowUOE; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.TreeUtils; + +/** Visitor for the {@link ModifiabilityChecker}. */ +public class ModifiabilityVisitor extends BaseTypeVisitor { + + public ModifiabilityVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + ExecutableElement method = TreeUtils.elementFromUse(node); + if (atypeFactory.getDeclAnnotation(method, WillThrowUOE.class) != null) { + checker.reportError(node, "usage.will.throw.uoe", method.getSimpleName()); + } + return super.visitMethodInvocation(node, p); + } + + // Suppress the framework’s “constructor result must be TOP” check. + // For Modifiability, constructors may legitimately produce @Modifiable. + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} +} diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index 4a88da7086a4..1991d3d4e110 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -113,10 +113,7 @@ void testEmptyCollections() { // :: error: (assignment) @Modifiable Map modM = Collections.emptyMap(); - @Unmodifiable Iterator it = Collections.emptyIterator(); - // Iterator.remove() is a mutating method - // :: error: (method.invocation) - it.remove(); + @Modifiable Iterator it = Collections.emptyIterator(); } void testFill() { diff --git a/framework/tests/all-systems/GenericsCasts.java b/framework/tests/all-systems/GenericsCasts.java index 4f43b5aeb54d..fa5adab8f467 100644 --- a/framework/tests/all-systems/GenericsCasts.java +++ b/framework/tests/all-systems/GenericsCasts.java @@ -1,6 +1,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.checkerframework.checker.modifiability.qual.Modifiable; public class GenericsCasts { // Cast from a raw type to a generic type @@ -26,6 +27,7 @@ void m() { // are currently problematic. // TODO: try to find a problem with skipping this check. class Test { + @Modifiable class Entry extends LinkedList {} class Queue { diff --git a/framework/tests/all-systems/GenericsEnclosing.java b/framework/tests/all-systems/GenericsEnclosing.java index 62e8ab1bf5f7..7aceb1881a19 100644 --- a/framework/tests/all-systems/GenericsEnclosing.java +++ b/framework/tests/all-systems/GenericsEnclosing.java @@ -1,4 +1,5 @@ import java.util.TreeMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** * Resolution of outer classes must take substitution of generic types into account. Thanks to EMS @@ -6,7 +7,7 @@ * *

Also see regex/GenericsEnclosing for a test case for the Regex Checker. */ -public class GenericsEnclosing extends TreeMap { +public @Modifiable class GenericsEnclosing extends TreeMap { class Inner { void foo() { put("string", "string".toString()); diff --git a/framework/tests/all-systems/RawTypeAssignment.java b/framework/tests/all-systems/RawTypeAssignment.java index 242db2396f18..880943ceb9b9 100644 --- a/framework/tests/all-systems/RawTypeAssignment.java +++ b/framework/tests/all-systems/RawTypeAssignment.java @@ -1,8 +1,10 @@ import java.util.ArrayList; import java.util.Calendar; +import org.checkerframework.checker.modifiability.qual.Modifiable; class Component {} +@Modifiable class Components extends ArrayList {} // If we include a type parameter in the superclass, then there is no error below. diff --git a/framework/tests/all-systems/java8inference/Issue3036.java b/framework/tests/all-systems/java8inference/Issue3036.java index 504f6aa86950..2d074ff3ac3f 100644 --- a/framework/tests/all-systems/java8inference/Issue3036.java +++ b/framework/tests/all-systems/java8inference/Issue3036.java @@ -6,6 +6,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.checkerframework.checker.modifiability.qual.Modifiable; public class Issue3036 { @@ -42,7 +43,7 @@ private void write(Stream stream) { .collect(Collectors.toList()); } - public static class ImmutableMap extends HashMap { + public static @Modifiable class ImmutableMap extends HashMap { public static ImmutableMap of(K k1, V v1, K k2, V v2) { throw new RuntimeException(); } From d397979033f2a54f783c0452f3e41c2333d65650 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 19 Jan 2026 00:19:21 -0800 Subject: [PATCH 022/308] added the type @WillThrowUOE, added type rule and test. TODO: update manual --- .../modifiability/qual/WillThrowUOE.java | 19 +++++++++++++ .../ModifiabilityAnnotatedTypeFactory.java | 20 ++++++++++++++ .../modifiability/ModifiabilityVisitor.java | 3 +++ .../checker/modifiability/messages.properties | 1 + .../tests/modifiability/WillThrowUOETest.java | 27 +++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java create mode 100644 checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties create mode 100644 checker/tests/modifiability/WillThrowUOETest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java new file mode 100644 index 000000000000..9a3341ee81e8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Indicates that this method will always throw {@link UnsupportedOperationException} when called. + * + *

This is a declaration annotation. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface WillThrowUOE {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 6ddbe968e69c..fcd0c94db786 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -22,14 +22,26 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory @SuppressWarnings("this-escape") public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); + // Cache the Element for java.lang.Iterable.iterator() to allow fast comparisons + // later. this.iteratorMethodElement = TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); + // Cache annotation mirrors for performance. this.UNMODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Unmodifiable.class); this.UNKNOWN_MODIFIABILITY = AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); postInit(); } + /** + * Refines the return type of the {@code iterator()} method based on the receiver's modifiability. + * + *

If a collection is {@code @Unmodifiable}, its iterator should also be treated as + * {@code @Unmodifiable} (meaning {@code remove()} cannot be called). + * + *

If a collection is {@code @Modifiable}, its iterator is {@code @UnknownModifiability} (the + * default behavior), which still permits calling {@code remove()}. + */ @Override public ParameterizedExecutableType methodFromUse( ExpressionTree tree, @@ -40,13 +52,21 @@ public ParameterizedExecutableType methodFromUse( ParameterizedExecutableType mType = super.methodFromUse(tree, methodElt, receiverType, inferTypeArgs); + // Special handling for the iterator() method. + // We want the modifiability of the Iterator to match the modifiability of the + // Collection. if (TreeUtils.isMethodInvocation(tree, iteratorMethodElement, processingEnv)) { AnnotatedTypeMirror returnType = mType.executableType.getReturnType(); if (receiverType.hasPrimaryAnnotation(Unmodifiable.class)) { + // collection.iterator() on an @Unmodifiable collection returns an @Unmodifiable + // Iterator. returnType.replaceAnnotation(UNMODIFIABLE); } else if (receiverType.hasPrimaryAnnotation(Modifiable.class)) { + // collection.iterator() on a @Modifiable collection returns an + // @UnknownModifiability Iterator + // (defaulting to bottom in this hierarchy is treated as safe for use). returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 9fe58a39f063..d0f9bd58c77c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -19,6 +19,9 @@ public ModifiabilityVisitor(BaseTypeChecker checker) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void p) { ExecutableElement method = TreeUtils.elementFromUse(node); + // Check if the method being invoked is annotated with @WillThrowUOE. + // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw + // UnsupportedOperationException at runtime, so we report an error immediately. if (atypeFactory.getDeclAnnotation(method, WillThrowUOE.class) != null) { checker.reportError(node, "usage.will.throw.uoe", method.getSimpleName()); } diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties new file mode 100644 index 000000000000..be9d69334519 --- /dev/null +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -0,0 +1 @@ +usage.will.throw.uoe=The method %s is annotated with @WillThrowUOE and will throw an UnsupportedOperationException if called. diff --git a/checker/tests/modifiability/WillThrowUOETest.java b/checker/tests/modifiability/WillThrowUOETest.java new file mode 100644 index 000000000000..a874185301a2 --- /dev/null +++ b/checker/tests/modifiability/WillThrowUOETest.java @@ -0,0 +1,27 @@ +import java.util.SortedSet; +import java.util.TreeSet; + +public class WillThrowUOETest { + + void testSortedSet(SortedSet s) { + // These methods are annotated with @WillThrowUOE in jdk.astub for SortedSet + + // :: error: (usage.will.throw.uoe) + s.addFirst("foo"); + + // :: error: (usage.will.throw.uoe) + s.addLast("bar"); + } + + void testImplementation(TreeSet ts) { + // Since TreeSet implements SortedSet, it inherits the methods. + // The stub file annotates the interface SortedSet. + // We verify that calling them on a concrete implementation also triggers the warning. + + // :: error: (usage.will.throw.uoe) + ts.addFirst("foo"); + + // :: error: (usage.will.throw.uoe) + ts.addLast("bar"); + } +} From 7bfbaa0b041a698dca858c4e85a978e6a0996a1b Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 21 Jan 2026 18:44:38 -0800 Subject: [PATCH 023/308] added sortedMap and sortedSet test for @WillThrowUOE --- .../tests/modifiability/WillThrowUOETest.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/checker/tests/modifiability/WillThrowUOETest.java b/checker/tests/modifiability/WillThrowUOETest.java index a874185301a2..8939163da09b 100644 --- a/checker/tests/modifiability/WillThrowUOETest.java +++ b/checker/tests/modifiability/WillThrowUOETest.java @@ -1,11 +1,11 @@ +import java.util.SortedMap; import java.util.SortedSet; +import java.util.TreeMap; import java.util.TreeSet; public class WillThrowUOETest { void testSortedSet(SortedSet s) { - // These methods are annotated with @WillThrowUOE in jdk.astub for SortedSet - // :: error: (usage.will.throw.uoe) s.addFirst("foo"); @@ -16,7 +16,8 @@ void testSortedSet(SortedSet s) { void testImplementation(TreeSet ts) { // Since TreeSet implements SortedSet, it inherits the methods. // The stub file annotates the interface SortedSet. - // We verify that calling them on a concrete implementation also triggers the warning. + // We verify that calling them on a concrete implementation also triggers the + // warning. // :: error: (usage.will.throw.uoe) ts.addFirst("foo"); @@ -24,4 +25,20 @@ void testImplementation(TreeSet ts) { // :: error: (usage.will.throw.uoe) ts.addLast("bar"); } + + void testSortedMap(SortedMap m) { + // :: error: (usage.will.throw.uoe) + m.putFirst("foo", "bar"); + + // :: error: (usage.will.throw.uoe) + m.putLast("baz", "qux"); + } + + void testImplementation(TreeMap tm) { + // :: error: (usage.will.throw.uoe) + tm.putFirst("foo", "bar"); + + // :: error: (usage.will.throw.uoe) + tm.putLast("baz", "qux"); + } } From 9801df7c812faba505aa463e7462c0440618675a Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 26 Jan 2026 14:43:59 -0800 Subject: [PATCH 024/308] queue tests --- .../modifiability/QueueModifiableTest.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 checker/tests/modifiability/QueueModifiableTest.java diff --git a/checker/tests/modifiability/QueueModifiableTest.java b/checker/tests/modifiability/QueueModifiableTest.java new file mode 100644 index 000000000000..43b22d37148c --- /dev/null +++ b/checker/tests/modifiability/QueueModifiableTest.java @@ -0,0 +1,132 @@ +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TransferQueue; +import org.checkerframework.checker.modifiability.qual.Modifiable; + +// Tests for Queue, Deque, BlockingQueue, etc. mutating and read-only methods with modifiability +// qualifiers. +// Per request, we only test Modifiable queues as creating truly unmodifiable queues is not +// standard. +public class QueueModifiableTest { + + void testQueue() { + @Modifiable Queue mod = new LinkedList<>(); + // Mutating + mod.add("a"); + mod.offer("b"); + mod.remove(); + mod.poll(); + mod.clear(); // inherited from Collection + + // Read-only + mod.element(); + mod.peek(); + } + + void testDeque() { + @Modifiable Deque mod = new ArrayDeque<>(); + // Mutating + mod.addFirst("a"); + mod.addLast("b"); + mod.offerFirst("c"); + mod.offerLast("d"); + mod.removeFirst(); + mod.removeLast(); + mod.pollFirst(); + mod.pollLast(); + mod.removeFirstOccurrence("a"); + mod.removeLastOccurrence("b"); + mod.push("e"); + mod.pop(); + + // Read-only + mod.getFirst(); + mod.getLast(); + mod.peekFirst(); + mod.peekLast(); + mod.descendingIterator(); + } + + void testBlockingQueue() throws InterruptedException { + @Modifiable BlockingQueue mod = new ArrayBlockingQueue<>(10); + // Mutating + mod.put("a"); + mod.offer("b", 1, TimeUnit.SECONDS); + mod.take(); + mod.poll(1, TimeUnit.SECONDS); + mod.drainTo(new LinkedList<>()); + mod.drainTo(new LinkedList<>(), 10); + + // Read-only + mod.remainingCapacity(); + } + + void testTransferQueue() throws InterruptedException { + @Modifiable TransferQueue mod = new LinkedTransferQueue<>(); + // Mutating + mod.transfer("a"); + mod.tryTransfer("b"); + mod.tryTransfer("c", 1, TimeUnit.SECONDS); + + // Read-only + mod.hasWaitingConsumer(); + mod.getWaitingConsumerCount(); + } + + void testBlockingDeque() throws InterruptedException { + @Modifiable BlockingDeque mod = new LinkedBlockingDeque<>(); + // Mutating + mod.putFirst("a"); + mod.putLast("b"); + mod.offerFirst("c", 1, TimeUnit.SECONDS); + mod.offerLast("d", 1, TimeUnit.SECONDS); + mod.takeFirst(); + mod.takeLast(); + mod.pollFirst(1, TimeUnit.SECONDS); + mod.pollLast(1, TimeUnit.SECONDS); + } + + void testArrayDeque() { + @Modifiable ArrayDeque mod = new ArrayDeque<>(); + mod.add("a"); + mod.clone(); + } + + void testPriorityQueue() { + @Modifiable PriorityQueue mod = new PriorityQueue<>(); + mod.add("a"); + Comparator c = mod.comparator(); + } + + void testUnmodifiableWrapper() { + Queue queue = new LinkedList<>(); + queue.add("initial"); + + // Case 1: Modifying an unmodifiable Collection view of a Queue + Collection unmodifiableQueue = Collections.unmodifiableCollection(queue); + // :: error: (method.invocation) + unmodifiableQueue.add("This will fail"); + + // Case 2: Iterator remove on Unmodifiable Collection + Collection unmodifiableQueue2 = Collections.unmodifiableCollection(queue); + Iterator it = unmodifiableQueue2.iterator(); + if (it.hasNext()) { + it.next(); + // :: error: (method.invocation) + it.remove(); + } + } +} From 6f4141565207c519788a6e71ac3414273de9be27 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 29 Jan 2026 17:09:15 -0800 Subject: [PATCH 025/308] added tests for IdentityHashMap and ConcurrentHashMao --- .../ConcurrentHashMapModifiableTest.java | 124 ++++++++++++++++++ .../IdentityHashMapModifiableTest.java | 60 +++++++++ 2 files changed, 184 insertions(+) create mode 100644 checker/tests/modifiability/ConcurrentHashMapModifiableTest.java create mode 100644 checker/tests/modifiability/IdentityHashMapModifiableTest.java diff --git a/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java b/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java new file mode 100644 index 000000000000..ce16912c970d --- /dev/null +++ b/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java @@ -0,0 +1,124 @@ +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; + +public class ConcurrentHashMapModifiableTest { + + void testBasicOperations() { + // 1. Class-level: it's a modifiable map + @Modifiable ConcurrentHashMap map = new ConcurrentHashMap<>(); + map.put("one", "1"); + map.remove("one"); + } + + void testIteratorEntries() { + @Modifiable ConcurrentHashMap map = new ConcurrentHashMap<>(); + map.put("one", "1"); + + // 2. entrySet() / iterators / spliterators: entries are mutable + for (Map.Entry entry : map.entrySet()) { + // Iterator returns modifiable entries for ConcurrentHashMap + entry.setValue("2"); // OK + } + + // Explicit iterator usage + Map.Entry explicitEntry = map.entrySet().iterator().next(); + explicitEntry.setValue("3"); // OK + } + + void testBulkOperations() { + @Modifiable ConcurrentHashMap map = new ConcurrentHashMap<>(); + map.put("one", "1"); + + // 3. Bulk operations on Map.Entry objects do not support method setValue + + // forEachEntry + map.forEachEntry( + 1, + entry -> { + // :: error: (method.invocation) + entry.setValue("3"); + }); + + // searchEntries + map.searchEntries( + 1, + entry -> { + // :: error: (method.invocation) + entry.setValue("3"); + return "result"; + }); + + // reduceEntries + map.reduceEntries( + 1, + entry -> { + // :: error: (method.invocation) + entry.setValue("3"); + return "result"; + }, + (a, b) -> a); + + // reduceEntriesToDouble + map.reduceEntriesToDouble( + 1, + entry -> { + // :: error: (method.invocation) + entry.setValue("3"); + return 0.0; + }, + 0.0, + Double::sum); + + // reduceEntriesToInt + map.reduceEntriesToInt( + 1, + entry -> { + // :: error: (method.invocation) + entry.setValue("3"); + return 0; + }, + 0, + Integer::sum); + + // reduceEntriesToLong + map.reduceEntriesToLong( + 1, + entry -> { + // :: error: (method.invocation) + entry.setValue("3"); + return 0L; + }, + 0L, + Long::sum); + + // reduceEntries (Entry reducer) - safe read-only usage + map.reduceEntries(1, (e1, e2) -> e1.getValue().length() >= e2.getValue().length() ? e1 : e2); + + // forEachEntry - safe read-only usage + map.forEachEntry( + 1, + entry -> { + if (entry.getValue().length() > 0) { + // read-only access + } + }); + + // searchEntries - safe read-only usage + map.searchEntries(1, entry -> entry.getValue().length() > 5 ? entry.getKey() : null); + } + + // pickFirst function can take in any Map.Entry mutability. + public static Map.Entry pickFirst(Map.Entry a, Map.Entry b) { + return a; // only reads, no setValue + } + + void invarianceDemo() { + @Modifiable ConcurrentHashMap m = new ConcurrentHashMap<>(); + m.put("k1", "v1"); + m.put("k2", "v2"); + + // To verify it compiles/runs in this environment: + Map.Entry e = m.reduceEntries(1L, ConcurrentHashMapModifiableTest::pickFirst); + } +} diff --git a/checker/tests/modifiability/IdentityHashMapModifiableTest.java b/checker/tests/modifiability/IdentityHashMapModifiableTest.java new file mode 100644 index 000000000000..8fded55f50aa --- /dev/null +++ b/checker/tests/modifiability/IdentityHashMapModifiableTest.java @@ -0,0 +1,60 @@ +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; +import org.checkerframework.checker.modifiability.qual.Modifiable; + +public class IdentityHashMapModifiableTest { + + void testIdentityHashMap() { + @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); + identityMap.put("key", "value1"); + identityMap.remove("key"); + + // IdentityHashMap itself is mutable + boolean removed = identityMap.remove("key", "value2"); + } + + // this type rule is not implemented yet + // void testViews() { + // @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); + + // // Views (keySet, values, entrySet) usually throw UOE on add operations + // // :: error: (method.invocation) + // identityMap.keySet().add("newKey"); + + // // :: error: (method.invocation) + // identityMap.values().add("newValue"); + + // // :: error: (method.invocation) + // identityMap.entrySet().add(new AbstractMap.SimpleEntry<>("newKey", "newValue")); + + // // However, removal is supported, so these should be allowed: + // identityMap.keySet().remove("key"); + // identityMap.values().remove("value"); + // identityMap.entrySet().remove(new AbstractMap.SimpleEntry<>("key", "value")); + // } + + void testEntries() { + @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); + identityMap.put("k", "v"); + + // Iterator returns mutable entries + Iterator> it = identityMap.entrySet().iterator(); + if (it.hasNext()) { + Map.Entry entry = it.next(); + // the following method is allowed and works at runtime, but the current type system throws + // an error because we could only make entrySet.iterator() and entrySet.stream() return the + // same mutability. + // :: error: (method.invocation) + entry.setValue("modified"); // OK + } + + // Stream/Spliterator returns immutable entries + if (!identityMap.isEmpty()) { + Map.Entry entry = identityMap.entrySet().stream().findFirst().get(); + // This throws UOE at runtime. + // :: error: (method.invocation) + entry.setValue("modified"); + } + } +} From 3de8d03ee98fd60e7f3beeef48d1b4bfdbb275d6 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 30 Jan 2026 18:09:40 -0800 Subject: [PATCH 026/308] added @WillThrowUOE to the manual --- .../tests/modifiability/WillThrowUOETest.java | 27 +++++++++++++++++++ docs/manual/modifiability-checker.tex | 16 +++++++++++ 2 files changed, 43 insertions(+) diff --git a/checker/tests/modifiability/WillThrowUOETest.java b/checker/tests/modifiability/WillThrowUOETest.java index 8939163da09b..dca783406e95 100644 --- a/checker/tests/modifiability/WillThrowUOETest.java +++ b/checker/tests/modifiability/WillThrowUOETest.java @@ -1,7 +1,10 @@ +import java.util.NavigableMap; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; public class WillThrowUOETest { @@ -41,4 +44,28 @@ void testImplementation(TreeMap tm) { // :: error: (usage.will.throw.uoe) tm.putLast("baz", "qux"); } + + void testNavigableMap(NavigableMap m) { + // :: error: (usage.will.throw.uoe) + m.putFirst("foo", "bar"); + + // :: error: (usage.will.throw.uoe) + m.putLast("baz", "qux"); + } + + void testConcurrentNavigableMap(ConcurrentNavigableMap m) { + // :: error: (usage.will.throw.uoe) + m.putFirst("foo", "bar"); + + // :: error: (usage.will.throw.uoe) + m.putLast("baz", "qux"); + } + + void testConcurrentSkipListMap(ConcurrentSkipListMap m) { + // :: error: (usage.will.throw.uoe) + m.putFirst("foo", "bar"); + + // :: error: (usage.will.throw.uoe) + m.putLast("baz", "qux"); + } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 01be735cd6bf..5a678398d3b0 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -94,6 +94,22 @@ \end{description} +\subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} + +The Modifiability Checker supports annotation that specify method +behavior. These are declaration annotations, not type annotations: they +apply to the method itself rather than to some particular type. + +\begin{description} + +\item[\code{@WillThrowUOE}] +This is a declaration annotation (not a type qualifier) that is applied to +method and constructor declarations. It indicates that the method always throws +\code{UnsupportedOperationException}. The Modifiability Checker issues an +error for any call to a method annotated with \code{@WillThrowUOE}. + +\end{description} + % TODO: % Each annotation’s Javadoc should include a precise description and an example % use, and should link back to this chapter using From aa512b5a12b6ef7663aabf2853e949568022fad7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 5 Feb 2026 17:16:10 -0800 Subject: [PATCH 027/308] init --- ...BottomModifiable.java => GrowReplace.java} | 13 +++++------ .../modifiability/qual/GrowShrink.java | 22 +++++++++++++++++++ .../checker/modifiability/qual/Growable.java | 22 +++++++++++++++++++ .../modifiability/qual/Modifiable.java | 6 ++--- .../modifiability/qual/PolyShrink.java | 22 +++++++++++++++++++ .../modifiability/qual/Replaceable.java | 22 +++++++++++++++++++ .../modifiability/qual/ShrinkReplace.java | 22 +++++++++++++++++++ .../modifiability/qual/Shrinkable.java | 22 +++++++++++++++++++ .../qual/UnknownModifiability.java | 3 ++- .../modifiability/qual/Unmodifiable.java | 2 -- .../ModifiabilityAnnotatedTypeFactory.java | 1 + 11 files changed, 143 insertions(+), 14 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{BottomModifiable.java => GrowReplace.java} (51%) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowReplace.java similarity index 51% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowReplace.java index 56fbf741771f..b3b55aa56a64 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowReplace.java @@ -5,21 +5,18 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; /** - * The bottom qualifier for the modifiability hierarchy. It is a subtype of both {@link Modifiable} - * and {@link Unmodifiable}. This qualifier is used internally by the Checker Framework to represent - * unreachable or contradictory states. + * Calling grow and replace operations on this collection will not result in throwing {@link + * UnsupportedOperationException}. * - *

Programmers should not write this annotation explicitly. + *

No guarantees are made about shrink operations. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({Modifiable.class, Unmodifiable.class}) -@InvisibleQualifier -public @interface BottomModifiable {} +@SubtypeOf({Growable.class, Replaceable.class}) +public @interface GrowReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java new file mode 100644 index 000000000000..564b19a35596 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling grow and shrink operations on this collection will not result in throwing {@link + * UnsupportedOperationException}. + * + *

No guarantees are made about replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Growable.class, Shrinkable.class}) +public @interface GrowShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java new file mode 100644 index 000000000000..3c233812a5f1 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling grow operations such as {@code add}, {@code addAll}, etc. on this collection will not + * result in throwing {@link UnsupportedOperationException}. + * + *

No guarantees are made about shrink or replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownModifiability.class) +public @interface Growable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index c10f11f478b6..07223188453f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -8,13 +8,13 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The collection is known to support all mutating operations such as {@code add}, {@code remove}, - * {@code clear}, etc., without throwing {@link UnsupportedOperationException}. + * Calling all mutating operations such as {@code add}, {@code remove}, {@code clear}, etc. on this + * collection will not result in throwing {@link UnsupportedOperationException}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownModifiability.class) +@SubtypeOf({GrowShrink.class, GrowReplace.class, ShrinkReplace.class}) public @interface Modifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java new file mode 100644 index 000000000000..6d5d9f5b036b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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 modifiability that preserves the shrink capability. + * + *

Use on methods that preserve shrinkability — for example, {@code Map.keySet()}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + *

TODO: need to implement + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(UnknownModifiability.class) +public @interface PolyShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java new file mode 100644 index 000000000000..20b7b79b1f56 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling replace operations such as {@code set}, {@code replaceAll}, etc. on this collection will + * not result in throwing {@link UnsupportedOperationException}. + * + *

No guarantees are made about grow or shrink operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownModifiability.class) +public @interface Replaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java new file mode 100644 index 000000000000..cd263b97f3ad --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling shrink and replace operations on this collection will not result in throwing {@link + * UnsupportedOperationException}. + * + *

No guarantees are made about grow operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Shrinkable.class, Replaceable.class}) +public @interface ShrinkReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java new file mode 100644 index 000000000000..45224b72d796 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling shrink operations such as {@code remove}, {@code clear}, etc. on this collection will not + * result in throwing {@link UnsupportedOperationException}. + * + *

No guarantees are made about grow or replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownModifiability.class) +public @interface Shrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java index d54e879a7219..4be6c33d1316 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java @@ -10,7 +10,8 @@ /** * Represents an unknown or arbitrary modifiability capability. The checker cannot determine whether - * the collection is modifiable or unmodifiable. This is the top qualifier in the hierarchy. + * the collection is growable, shrinkable, or replaceable. This is the top qualifier in the + * hierarchy. * *

This is the default qualifier for unannotated types. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index b4decae44783..3d2799a7288f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -5,7 +5,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** * The collection is known to be unmodifiable: calling a mutating method such as {@code add} or @@ -16,5 +15,4 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownModifiability.class) public @interface Unmodifiable {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index fcd0c94db786..f86d60876073 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -30,6 +30,7 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNMODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Unmodifiable.class); this.UNKNOWN_MODIFIABILITY = AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); + addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_MODIFIABILITY); postInit(); } From 24fc8107222e0dc6ab43e5164cab22ee5582d9d0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 6 Feb 2026 22:56:10 -0800 Subject: [PATCH 028/308] added unmodifiable alias --- .../ModifiabilityAnnotatedTypeFactory.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index f86d60876073..d76857bf70ae 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -1,9 +1,19 @@ package org.checkerframework.checker.modifiability; import com.sun.source.tree.ExpressionTree; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.qual.GrowReplace; +import org.checkerframework.checker.modifiability.qual.GrowShrink; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.ShrinkReplace; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -16,7 +26,6 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private final ExecutableElement iteratorMethodElement; - private final AnnotationMirror UNMODIFIABLE; private final AnnotationMirror UNKNOWN_MODIFIABILITY; @SuppressWarnings("this-escape") @@ -27,13 +36,26 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { this.iteratorMethodElement = TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); // Cache annotation mirrors for performance. - this.UNMODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Unmodifiable.class); this.UNKNOWN_MODIFIABILITY = AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_MODIFIABILITY); postInit(); } + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + UnknownModifiability.class, + Modifiable.class, + GrowReplace.class, + GrowShrink.class, + Growable.class, + Replaceable.class, + ShrinkReplace.class, + Shrinkable.class)); + } + /** * Refines the return type of the {@code iterator()} method based on the receiver's modifiability. * @@ -63,7 +85,7 @@ public ParameterizedExecutableType methodFromUse( if (receiverType.hasPrimaryAnnotation(Unmodifiable.class)) { // collection.iterator() on an @Unmodifiable collection returns an @Unmodifiable // Iterator. - returnType.replaceAnnotation(UNMODIFIABLE); + returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); } else if (receiverType.hasPrimaryAnnotation(Modifiable.class)) { // collection.iterator() on a @Modifiable collection returns an // @UnknownModifiability Iterator From 6de4c4d3976a0cddd7e9c56f70f58cf73525a0f7 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 7 Feb 2026 10:25:25 -0800 Subject: [PATCH 029/308] Add `-AinferOutputDirectory` command-line argument --- docs/CHANGELOG.md | 5 +- docs/manual/introduction.tex | 5 ++ ...holeProgramInferenceJavaParserStorage.java | 52 ++++++++++--------- .../WholeProgramInferenceScenesStorage.java | 28 +++++----- .../framework/source/SourceChecker.java | 3 ++ .../framework/type/AnnotatedTypeFactory.java | 17 ++++-- 6 files changed, 66 insertions(+), 44 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2f535e8de27f..4a383a057d31 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,7 +7,10 @@ ### User-visible changes -Removed long-deprecated `-Alint=forbidnonnullarraycomponents`. +Command-line arguments: + +* Added `-AinferOutputDirectory`. +* Removed long-deprecated `-Alint=forbidnonnullarraycomponents`. ### Implementation details diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 87d17b60d9f8..74a8c2133bf1 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -693,6 +693,11 @@ Using \<-Ainfer=stubs> produces \<.astub> files. Using \<-Ainfer=ajava> produces \<.ajava> files. You must also supply \<-Awarns>, or the inference output may be incomplete. +\item \<-AinferOutputDirectory> + The directory into which to write inference output (e.g., \<.ajava> + files). It defaults to \, but when using + a build system (e.g., gradle), the default may be relative to a build + system directory rather than the project root directory. \item \<-AinferOutputOriginal> When outputting \<.ajava> files when running with \<-Ainfer=ajava>, also output a copy of the original file with no inferred annotations, diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index 2449573351e3..07cbe1a11553 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -45,6 +45,7 @@ import java.lang.annotation.Annotation; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -93,6 +94,7 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; +import org.checkerframework.javacutil.UserError; import org.plumelib.util.ArraySet; import org.plumelib.util.CollectionsPlume; import org.plumelib.util.DeepCopyable; @@ -108,12 +110,6 @@ public class WholeProgramInferenceJavaParserStorage implements WholeProgramInferenceStorage { - /** - * Directory where .ajava files will be written to and read from. This directory is relative to - * where the javac command is executed. - */ - public static final File AJAVA_FILES_PATH = new File("build", "whole-program-inference"); - /** The type factory associated with this. */ protected final AnnotatedTypeFactory atypeFactory; @@ -144,6 +140,9 @@ public class WholeProgramInferenceJavaParserStorage /** Maps from binary class name to the source file that contains it. */ private Map classToSource = new HashMap<>(); + /** The directory for reading and writing .ajava files. */ + private final Path inferOutputDirectory; + /** True if the {@code -AinferOutputOriginal} option was supplied to the checker. */ private final boolean inferOutputOriginal; @@ -178,12 +177,14 @@ public static boolean isInvisible(Class qual) { * annotations. * * @param atypeFactory the associated type factory + * @param inferOutputDirectory the directory into which to write whole program inference files * @param inferOutputOriginal true if the -AinferOutputOriginal option was supplied to the checker */ public WholeProgramInferenceJavaParserStorage( - AnnotatedTypeFactory atypeFactory, boolean inferOutputOriginal) { + AnnotatedTypeFactory atypeFactory, String inferOutputDirectory, boolean inferOutputOriginal) { this.atypeFactory = atypeFactory; this.elements = atypeFactory.getElementUtils(); + this.inferOutputDirectory = Path.of(inferOutputDirectory); this.inferOutputOriginal = inferOutputOriginal; } @@ -1033,9 +1034,10 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke throw new BugInCF("WholeProgramInferenceJavaParser used with output format " + outputFormat); } - File outputDir = AJAVA_FILES_PATH; - if (!outputDir.exists()) { - outputDir.mkdirs(); + try { + Files.createDirectories(inferOutputDirectory); + } catch (IOException e) { + throw new UserError("Cannot create " + inferOutputDirectory.toAbsolutePath(), e); } setSupertypesAndSubtypesModified(); @@ -1045,13 +1047,12 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke // effects that we don't want to be persistent. CompilationUnitAnnos root = sourceToAnnos.get(path).deepCopy(); wpiPrepareCompilationUnitForWriting(root); - File packageDir; + Path packageDir; if (!root.compilationUnit.getPackageDeclaration().isPresent()) { - packageDir = AJAVA_FILES_PATH; + packageDir = inferOutputDirectory; } else { packageDir = - new File( - AJAVA_FILES_PATH, + inferOutputDirectory.resolve( root.compilationUnit .getPackageDeclaration() .get() @@ -1059,8 +1060,10 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke .replaceAll("\\.", File.separator)); } - if (!packageDir.exists()) { - packageDir.mkdirs(); + try { + Files.createDirectories(packageDir); + } catch (IOException e) { + throw new UserError("Cannot create " + packageDir.toAbsolutePath()); } String name = new File(path).getName(); @@ -1069,11 +1072,11 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke } String nameWithChecker = name + "-" + checker.getClass().getCanonicalName() + ".ajava"; - File outputPath = new File(packageDir, nameWithChecker); + Path outputPath = packageDir.resolve(nameWithChecker); if (this.inferOutputOriginal) { - File outputPathNoCheckerName = new File(packageDir, name + ".ajava"); + Path outputPathNoCheckerName = packageDir.resolve(name + ".ajava"); // Avoid re-writing this file for each checker that was run. - if (Files.notExists(outputPathNoCheckerName.toPath())) { + if (Files.notExists(outputPathNoCheckerName)) { writeAjavaFile(outputPathNoCheckerName, root); } } @@ -1090,13 +1093,13 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke * @param outputPath the path to which the ajava file should be written * @param root the compilation unit to be written */ - private void writeAjavaFile(File outputPath, CompilationUnitAnnos root) { - try (Writer writer = Files.newBufferedWriter(outputPath.toPath(), StandardCharsets.UTF_8)) { + private void writeAjavaFile(Path outputPath, CompilationUnitAnnos root) { + try (Writer writer = Files.newBufferedWriter(outputPath, StandardCharsets.UTF_8)) { // This commented implementation uses JavaParser's lexical preserving printing, which // writes the file such that its formatting is close to the original source file it was - // parsed from as possible. It is commented out because this feature is very buggy and - // crashes when adding annotations in certain locations. + // parsed from as possible. It is commented out because the JavaParser feature is very buggy + // and crashes when adding annotations in certain locations. // LexicalPreservingPrinter.print(root.declaration, writer); // Do not print invisible qualifiers, to avoid cluttering the output. @@ -1158,7 +1161,8 @@ public void visit(final StringLiteralExpr n, final Void arg) { } }; - writer.write(prettyPrinter.print(root.compilationUnit)); + String fileContent = prettyPrinter.print(root.compilationUnit); + writer.write(fileContent); } catch (IOException e) { throw new BugInCF("Error while writing ajava file " + outputPath, e); } diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java index 98eb3de83bf6..9fc3d3d5f436 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.IOException; import java.lang.annotation.Target; +import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -66,20 +67,13 @@ * if using {@link OutputFormat#STUB}. * *

This class populates the initial Scenes by reading existing .jaif files on the {@link - * #JAIF_FILES_PATH} directory (regardless of output format). Having more information in those + * #inferOutputDirectory} directory (regardless of output format). Having more information in those * initial .jaif files means that the precision achieved by the whole-program inference analysis * will be better. {@link #writeScenes} rewrites the initial .jaif files and may create new ones. */ public class WholeProgramInferenceScenesStorage implements WholeProgramInferenceStorage { - /** - * Directory where .jaif files will be written to and read from. This directory is relative to - * where the CF's javac command is executed. - */ - public static final String JAIF_FILES_PATH = - "build" + File.separator + "whole-program-inference" + File.separator; - /** The type factory associated with this WholeProgramInferenceScenesStorage. */ protected final AnnotatedTypeFactory atypeFactory; @@ -97,13 +91,13 @@ public class WholeProgramInferenceScenesStorage */ private final boolean ignoreNullAssignments; - /** Maps .jaif file paths (Strings) to Scenes. Relative to JAIF_FILES_PATH. */ + /** Maps .jaif file paths (Strings) to Scenes. Relative to inferOutputDirectory. */ public final Map scenes = new HashMap<>(); /** * Scenes that were modified since the last time all Scenes were written into .jaif files. Each - * String element of this set is a path (relative to JAIF_FILES_PATH) to the .jaif file of the - * corresponding Scene in the set. It is obtained by passing a class name as argument to the + * String element of this set is a path (relative to inferOutputDirectory) to the .jaif file of + * the corresponding Scene in the set. It is obtained by passing a class name as argument to the * {@link #getJaifPath} method. * *

Modifying a Scene means adding (or changing) a type annotation for a field, method return @@ -134,16 +128,22 @@ public class WholeProgramInferenceScenesStorage */ private final Map postconditionsToDeclaredTypes = new HashMap<>(); + /** The directory for reading and writing .jaif files. */ + private final Path inferOutputDirectory; + /** * Default constructor. * * @param atypeFactory the type factory associated with this WholeProgramInferenceScenesStorage + * @param inferOutputDirectory the directory into which to write whole program inference files */ - public WholeProgramInferenceScenesStorage(AnnotatedTypeFactory atypeFactory) { + public WholeProgramInferenceScenesStorage( + AnnotatedTypeFactory atypeFactory, String inferOutputDirectory) { this.atypeFactory = atypeFactory; boolean isNullness = atypeFactory.getClass().getSimpleName().equals("NullnessAnnotatedTypeFactory"); this.ignoreNullAssignments = !isNullness; + this.inferOutputDirectory = Path.of(inferOutputDirectory); } @Override @@ -478,7 +478,7 @@ public boolean addClassDeclarationAnnotation(TypeElement classElt, AnnotationMir */ public void writeScenes(OutputFormat outputFormat, BaseTypeChecker checker) { // Create WPI directory if it doesn't exist already. - File jaifDir = new File(JAIF_FILES_PATH); + File jaifDir = inferOutputDirectory.toFile(); if (!jaifDir.exists()) { jaifDir.mkdirs(); } @@ -496,7 +496,7 @@ public void writeScenes(OutputFormat outputFormat, BaseTypeChecker checker) { * @return the path to the .jaif file */ protected String getJaifPath(String className) { - String jaifPath = JAIF_FILES_PATH + className + ".jaif"; + String jaifPath = inferOutputDirectory + className + ".jaif"; return jaifPath; } 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 ffa344b7672c..e34353da6339 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -220,6 +220,9 @@ // "-Ainfer=stubs" or "-Ainfer=jaifs". "infer", + // The directory into which to write whole-program inference results. + "inferOutputDirectory", + // Whether to output a copy of each file for which annotations were inferred, formatted // as an ajava file. Can only be used with -Ainfer=ajava "inferOutputOriginal", diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 221ecc089b90..904d39bb48a3 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -619,7 +619,9 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { this.annotationFormatter = createAnnotationFormatter(); this.typeInformationPresenter = createTypeInformationPresenter(); - if (checker.hasOption("infer")) { + if (!checker.hasOption("infer")) { + wholeProgramInference = null; + } else { checkInvalidOptionsInferSignatures(); String inferArg = checker.getOption("infer"); // No argument means "jaifs", for (temporary) backwards compatibility. @@ -643,6 +645,8 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { + " should be one of: -Ainfer=jaifs, -Ainfer=stubs, -Ainfer=ajava"); } boolean showWpiFailedInferences = checker.hasOption("showWpiFailedInferences"); + String inferOutputDirectory = + checker.getOption("inferOutputDirectory", "build/whole-program-inference"); boolean inferOutputOriginal = checker.hasOption("inferOutputOriginal"); if (inferOutputOriginal && wpiOutputFormat != WholeProgramInference.OutputFormat.AJAVA) { checker.message( @@ -653,20 +657,21 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { wholeProgramInference = new WholeProgramInferenceImplementation( this, - new WholeProgramInferenceJavaParserStorage(this, inferOutputOriginal), + new WholeProgramInferenceJavaParserStorage( + this, inferOutputDirectory, inferOutputOriginal), showWpiFailedInferences); } else { wholeProgramInference = new WholeProgramInferenceImplementation( - this, new WholeProgramInferenceScenesStorage(this), showWpiFailedInferences); + this, + new WholeProgramInferenceScenesStorage(this, inferOutputDirectory), + showWpiFailedInferences); } if (!checker.hasOption("warns")) { // Without -Awarns, the inference output may be incomplete, because javac halts // after issuing an error. checker.message(Diagnostic.Kind.ERROR, "Do not supply -Ainfer without -Awarns"); } - } else { - wholeProgramInference = null; } ignoreRawTypeArguments = checker.getBooleanOption("ignoreRawTypeArguments", true); @@ -1458,6 +1463,8 @@ public void preProcessClassTree(ClassTree classTree) {} *

The default implementation uses this to store the defaulted AnnotatedTypeMirrors and * inherited declaration annotations back into the corresponding Elements. Subclasses might want * to override this method if storing defaulted types is not desirable. + * + * @param tree the class to postprocess */ public void postProcessClassTree(ClassTree tree) { TypesIntoElements.store(processingEnv, this, tree); From ec42250b9aea70821228943817432f692d35c9be Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 7 Feb 2026 12:00:12 -0800 Subject: [PATCH 030/308] Update docs/manual/introduction.tex Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/manual/introduction.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 74a8c2133bf1..9a24abb4dfbd 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -696,7 +696,7 @@ \item \<-AinferOutputDirectory> The directory into which to write inference output (e.g., \<.ajava> files). It defaults to \, but when using - a build system (e.g., gradle), the default may be relative to a build + a build system (e.g., Gradle), the default may be relative to a build system directory rather than the project root directory. \item \<-AinferOutputOriginal> When outputting \<.ajava> files when running with \<-Ainfer=ajava>, From 24429c4f96400d5f07801f16fbce2a36003e494c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 7 Feb 2026 12:03:50 -0800 Subject: [PATCH 031/308] Tweaks --- docs/manual/introduction.tex | 1 + .../WholeProgramInferenceJavaParserStorage.java | 2 +- .../WholeProgramInferenceScenesStorage.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 74a8c2133bf1..29360f972fc9 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -1714,3 +1714,4 @@ % LocalWords: AshowPrefixInWarningMessages AstubNoWarnIfNotFound % LocalWords: AshowWpiFailedInferences AassumePureGetters AonlyFiles AskipFiles % LocalWords: AexceptionLineSeparator AslowTypecheckingSeconds +% LocalWords: AinferOutputDirectory diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index 07cbe1a11553..633bd277a5aa 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -1063,7 +1063,7 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke try { Files.createDirectories(packageDir); } catch (IOException e) { - throw new UserError("Cannot create " + packageDir.toAbsolutePath()); + throw new UserError("Cannot create " + packageDir.toAbsolutePath(), e); } String name = new File(path).getName(); diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java index 9fc3d3d5f436..74f915917782 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java @@ -496,7 +496,7 @@ public void writeScenes(OutputFormat outputFormat, BaseTypeChecker checker) { * @return the path to the .jaif file */ protected String getJaifPath(String className) { - String jaifPath = inferOutputDirectory + className + ".jaif"; + String jaifPath = inferOutputDirectory.resolve(className + ".jaif").toString(); return jaifPath; } From 9427bf30f474127b2e93f8bc3c3c1cb7c17052df Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 7 Feb 2026 12:42:31 -0800 Subject: [PATCH 032/308] Suppress a false positive --- .../WholeProgramInferenceJavaParserStorage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index 633bd277a5aa..a3622deb1617 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -1028,6 +1028,7 @@ public void wpiPrepareMethodForWriting( atypeFactory.wpiPrepareMethodForWriting(methodAnnos, inSupertypes, inSubtypes); } + @SuppressWarnings("optionalimpl:prefer.map.and.orelse") // false positive (`resolve()` takes args) @Override public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checker) { if (outputFormat != OutputFormat.AJAVA) { From 5f99411257f0083fcc03bab26ed902f7e58f7da1 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 7 Feb 2026 14:10:09 -0800 Subject: [PATCH 033/308] Shallow clone --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 52c2cb41e3ca..daae479d3b36 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,6 @@ all default: style-check # Code style; defines `style-check` and `style-fix`. ifeq (,$(wildcard .plume-scripts)) -dummy := $(shell git clone -q https://github.com/plume-lib/plume-scripts.git .plume-scripts) +dummy := $(shell git clone --depth=1 -q https://github.com/plume-lib/plume-scripts.git .plume-scripts) endif include .plume-scripts/code-style.mak From 9abbd2f84d6fa482bda05f5c687826714a37dba0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 7 Feb 2026 23:57:52 -0800 Subject: [PATCH 034/308] updated hierarchy test and polymodifiable test, now all tests passed with the new lattice. TODO: add tests and rules for set. --- .../ModifiabilityAnnotatedTypeFactory.java | 2 + .../tests/modifiability/HierarchyTest.java | 177 +++++++++++++++--- .../modifiability/PolyModifiableTest.java | 107 ++++++++--- 3 files changed, 233 insertions(+), 53 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index d76857bf70ae..6d671d7e739f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -11,6 +11,7 @@ import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.ShrinkReplace; import org.checkerframework.checker.modifiability.qual.Shrinkable; @@ -48,6 +49,7 @@ protected Set> createSupportedTypeQualifiers() { Arrays.asList( UnknownModifiability.class, Modifiable.class, + PolyModifiable.class, GrowReplace.class, GrowShrink.class, Growable.class, diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index 59bf6f58c2aa..0254220ad444 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -1,60 +1,177 @@ -import org.checkerframework.checker.modifiability.qual.BottomModifiable; +import org.checkerframework.checker.modifiability.qual.GrowReplace; +import org.checkerframework.checker.modifiability.qual.GrowShrink; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.ShrinkReplace; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; -/** - * Tests the subtyping relationships among the modifiability qualifiers: @UnknownModifiability (top) - * / \ @Modifiable @Unmodifiable \ / @BottomModifiable (bottom) - */ class HierarchyTest { - - void testHierarchy( - @UnknownModifiability Object any, + void test( @Modifiable Object mod, - @Unmodifiable Object unmod, - @BottomModifiable Object b) { + @GrowShrink Object gs, + @GrowReplace Object gr, + @ShrinkReplace Object sr, + @Growable Object g, + @Shrinkable Object s, + @Replaceable Object r, + @UnknownModifiability Object unknown, + @Unmodifiable Object unmod) { + // ============================================================ + // Assignments to @UnknownModifiability (Top) + // ============================================================ + @UnknownModifiability Object u1 = mod; + @UnknownModifiability Object u2 = gs; + @UnknownModifiability Object u3 = gr; + @UnknownModifiability Object u4 = sr; + @UnknownModifiability Object u5 = g; + @UnknownModifiability Object u6 = s; + @UnknownModifiability Object u7 = r; + @UnknownModifiability Object u8 = unknown; + @UnknownModifiability Object u9 = unmod; // ============================================================ - // Assignments to @UnknownModifiability (top) + // Assignments to @Unmodifiable (Alias to Top) // ============================================================ + @Unmodifiable Object um1 = mod; + @Unmodifiable Object um2 = gs; + @Unmodifiable Object um3 = gr; + @Unmodifiable Object um4 = sr; + @Unmodifiable Object um5 = g; + @Unmodifiable Object um6 = s; + @Unmodifiable Object um7 = r; + @Unmodifiable Object um8 = unknown; + @Unmodifiable Object um9 = unmod; - @UnknownModifiability Object a1 = any; // OK - @UnknownModifiability Object a2 = mod; // OK: Modifiable <: Unknown - @UnknownModifiability Object a3 = unmod; // OK: Unmodifiable <: Unknown - @UnknownModifiability Object a4 = b; // OK: Bottom <: Unknown // ============================================================ - // Assignments to @Modifiable + // Assignments to @Growable (G) // ============================================================ + @Growable Object g1 = mod; + @Growable Object g2 = gs; + @Growable Object g3 = gr; + // :: error: (assignment) + @Growable Object g4 = sr; + @Growable Object g5 = g; + // :: error: (assignment) + @Growable Object g6 = s; + // :: error: (assignment) + @Growable Object g7 = r; + // :: error: (assignment) + @Growable Object g8 = unknown; + // :: error: (assignment) + @Growable Object g9 = unmod; - @Modifiable Object m1 = mod; // OK + // ============================================================ + // Assignments to @Shrinkable (S) + // ============================================================ + @Shrinkable Object s1 = mod; + @Shrinkable Object s2 = gs; + // :: error: (assignment) + @Shrinkable Object s3 = gr; + @Shrinkable Object s4 = sr; + // :: error: (assignment) + @Shrinkable Object s5 = g; + @Shrinkable Object s6 = s; // :: error: (assignment) - @Modifiable Object m2 = any; // Error: Unknown → Modifiable not allowed + @Shrinkable Object s7 = r; // :: error: (assignment) - @Modifiable Object m3 = unmod; // Error: Unmodifiable → Modifiable not allowed - @Modifiable Object m4 = b; // OK: Bottom <: Modifiable + @Shrinkable Object s8 = unknown; + // :: error: (assignment) + @Shrinkable Object s9 = unmod; // ============================================================ - // Assignments to @Unmodifiable + // Assignments to @Replaceable (R) // ============================================================ + @Replaceable Object r1 = mod; + // :: error: (assignment) + @Replaceable Object r2 = gs; + @Replaceable Object r3 = gr; + @Replaceable Object r4 = sr; + // :: error: (assignment) + @Replaceable Object r5 = g; + // :: error: (assignment) + @Replaceable Object r6 = s; + @Replaceable Object r7 = r; + // :: error: (assignment) + @Replaceable Object r8 = unknown; + // :: error: (assignment) + @Replaceable Object r9 = unmod; + + // ============================================================ + // Assignments to @GrowShrink (G, S) + // ============================================================ + @GrowShrink Object gs1 = mod; + @GrowShrink Object gs2 = gs; + // :: error: (assignment) + @GrowShrink Object gs3 = gr; + // :: error: (assignment) + @GrowShrink Object gs4 = sr; + // :: error: (assignment) + @GrowShrink Object gs5 = g; + // :: error: (assignment) + @GrowShrink Object gs6 = s; + // :: error: (assignment) + @GrowShrink Object gs7 = r; + // :: error: (assignment) + @GrowShrink Object gs8 = unknown; - @Unmodifiable Object u1 = unmod; // OK + // ============================================================ + // Assignments to @GrowReplace (G, R) + // ============================================================ + @GrowReplace Object gr1 = mod; // :: error: (assignment) - @Unmodifiable Object u2 = any; // Error: Unknown → Unmodifiable not allowed + @GrowReplace Object gr2 = gs; + @GrowReplace Object gr3 = gr; // :: error: (assignment) - @Unmodifiable Object u3 = mod; // Error: Modifiable → Unmodifiable not allowed - @Unmodifiable Object u4 = b; // OK: Bottom <: Unmodifiable + @GrowReplace Object gr4 = sr; + // :: error: (assignment) + @GrowReplace Object gr5 = g; + // :: error: (assignment) + @GrowReplace Object gr6 = s; + // :: error: (assignment) + @GrowReplace Object gr7 = r; + // :: error: (assignment) + @GrowReplace Object gr8 = unknown; // ============================================================ - // Assignments to @BottomModifiable (bottom) + // Assignments to @ShrinkReplace (S, R) // ============================================================ + @ShrinkReplace Object sr1 = mod; + // :: error: (assignment) + @ShrinkReplace Object sr2 = gs; + // :: error: (assignment) + @ShrinkReplace Object sr3 = gr; + @ShrinkReplace Object sr4 = sr; + // :: error: (assignment) + @ShrinkReplace Object sr5 = g; + // :: error: (assignment) + @ShrinkReplace Object sr6 = s; + // :: error: (assignment) + @ShrinkReplace Object sr7 = r; + // :: error: (assignment) + @ShrinkReplace Object sr8 = unknown; - @BottomModifiable Object b1 = b; // OK + // ============================================================ + // Assignments to @Modifiable (Bottom) + // ============================================================ + @Modifiable Object m1 = mod; + // :: error: (assignment) + @Modifiable Object m2 = gs; + // :: error: (assignment) + @Modifiable Object m3 = gr; + // :: error: (assignment) + @Modifiable Object m4 = sr; + // :: error: (assignment) + @Modifiable Object m5 = g; + // :: error: (assignment) + @Modifiable Object m6 = s; // :: error: (assignment) - @BottomModifiable Object b2 = any; // Error: Unknown → Bottom not allowed + @Modifiable Object m7 = r; // :: error: (assignment) - @BottomModifiable Object b3 = mod; // Error: Modifiable → Bottom not allowed + @Modifiable Object m8 = unknown; // :: error: (assignment) - @BottomModifiable Object b4 = unmod; // Error: Unmodifiable → Bottom not allowed + @Modifiable Object m9 = unmod; } } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 0a5980d3ad86..f6f53add9e6d 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,49 +1,110 @@ -import org.checkerframework.checker.modifiability.qual.BottomModifiable; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.GrowReplace; +import org.checkerframework.checker.modifiability.qual.GrowShrink; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.ShrinkReplace; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; public class PolyModifiableTest { // A simple polymorphic method @PolyModifiable - Object identity(@PolyModifiable Object x) { + List identity(@PolyModifiable List x) { return x; } void testPoly( - @Modifiable Object mod, - @Unmodifiable Object unmod, - @UnknownModifiability Object any, - @BottomModifiable Object bottom) { + @Modifiable List mod, + @GrowShrink List gs, + @GrowReplace List gr, + @ShrinkReplace List sr, + @Growable List g, + @Shrinkable List s, + @Replaceable List r, + @UnknownModifiability List unknown, + @Unmodifiable List unmod) { - // Allowed: preserving same modifiability - @Modifiable Object a = identity(mod); - @Unmodifiable Object b = identity(unmod); - @UnknownModifiability Object c = identity(any); - @BottomModifiable Object d = identity(bottom); + // ============================================================ + // Identity on Modifiable (Bottom - 111) + // ============================================================ + @Modifiable List m1 = identity(mod); // OK + @UnknownModifiability List m2 = identity(mod); // OK + // ============================================================ + // Identity on GrowShrink (110) + // ============================================================ + @GrowShrink List gs1 = identity(gs); // OK // :: error: (assignment) - @Modifiable Object e = identity(unmod); // cannot upcast from Unmodifiable → Modifiable + @Modifiable List gs2 = identity(gs); // Error: GS (110) !<: Mod (111) + @Growable List gs3 = identity(gs); // OK: GS (110) <: G (100) + @Shrinkable List gs4 = identity(gs); // OK: GS (110) <: S (010) // :: error: (assignment) - @Modifiable Object f = identity(any); // cannot upcast from Unknown → Modifiable - @Modifiable Object h = identity(bottom); // ok to assign Bottom → Modifiable + @Replaceable List gs5 = identity(gs); // Error: GS (110) !<: R (001) + // ============================================================ + // Identity on GrowReplace (101) + // ============================================================ + @GrowReplace List gr1 = identity(gr); // OK // :: error: (assignment) - @Unmodifiable Object i = identity(mod); // cannot downcast from Modifiable → Unmodifiable + @Modifiable List gr2 = identity(gr); // Error + @Growable List gr3 = identity(gr); // OK // :: error: (assignment) - @Unmodifiable Object j = identity(any); // cannot downcast from Unknown → Unmodifiable - @Unmodifiable Object l = identity(bottom); // ok to assign Bottom → Unmodifiable + @Shrinkable List gr4 = identity(gr); // Error + @Replaceable List gr5 = identity(gr); // OK - @UnknownModifiability Object m = identity(mod); // ok to assign Modifiable → Unknown - @UnknownModifiability Object n = identity(unmod); // ok to assign Unmodifiable → Unknown - @UnknownModifiability Object o = identity(bottom); // ok to assign Bottom → Unknown + // ============================================================ + // Identity on ShrinkReplace (011) + // ============================================================ + @ShrinkReplace List sr1 = identity(sr); // OK + // :: error: (assignment) + @Modifiable List sr2 = identity(sr); // Error + // :: error: (assignment) + @Growable List sr3 = identity(sr); // Error + @Shrinkable List sr4 = identity(sr); // OK + @Replaceable List sr5 = identity(sr); // OK + + // ============================================================ + // Identity on Growable (100) + // ============================================================ + @Growable List g1 = identity(g); // OK + // :: error: (assignment) + @GrowShrink List g2 = identity(g); // Error + // :: error: (assignment) + @Modifiable List g3 = identity(g); // Error + // ============================================================ + // Identity on Shrinkable (010) + // ============================================================ + @Shrinkable List s1 = identity(s); // OK + // :: error: (assignment) + @GrowShrink List s2 = identity(s); // Error + // :: error: (assignment) + @Modifiable List s3 = identity(s); // Error + // ============================================================ + // Identity on Replaceable (001) + // ============================================================ + @Replaceable List r1 = identity(r); // OK + // :: error: (assignment) + @GrowReplace List r2 = identity(r); // Error + // :: error: (assignment) + @Modifiable List r3 = identity(r); // Error + // ============================================================ + // Identity on UnknownModifiability (000 / Top) + // ============================================================ + @UnknownModifiability List u1 = identity(unknown); // OK // :: error: (assignment) - @BottomModifiable Object p = identity(mod); // cannot upcast from Modifiable → Bottom + @Modifiable List u2 = identity(unknown); // Error // :: error: (assignment) - @BottomModifiable Object q = identity(unmod); // cannot upcast from Unmodifiable → Bottom + @Growable List u3 = identity(unknown); // Error + // ============================================================ + // Identity on Unmodifiable (000 / Top alias) + // ============================================================ + @Unmodifiable List unmod1 = identity(unmod); // OK // :: error: (assignment) - @BottomModifiable Object r = identity(any); // cannot upcast from Unknown → Bottom + @Modifiable List unmod2 = identity(unmod); // Error } } From ae2489b6a1a8453f4bee1972d0630a76990c1e0e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 8 Feb 2026 16:45:30 -0800 Subject: [PATCH 035/308] Set `set` `iterator` and `Map.Entry` toward top. So a @replaceable set is @Unknown and @Modifiable set is @GrowShrink etc. Tested. --- .../ModifiabilityAnnotatedTypeFactory.java | 117 +++++++++++++++ checker/tests/modifiability/SetTest.java | 140 ++++++++++++++++++ .../tests/modifiability/WillThrowUOETest.java | 1 - 3 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 checker/tests/modifiability/SetTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 6d671d7e739f..efac421d6407 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -7,6 +7,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.qual.GrowReplace; import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; @@ -21,6 +22,9 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; @@ -29,6 +33,21 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory private final ExecutableElement iteratorMethodElement; private final AnnotationMirror UNKNOWN_MODIFIABILITY; + // Cached Types for default handling + private final TypeMirror setType; + private final TypeMirror mapEntryType; + private final TypeMirror iteratorType; + + // Cached Qualifiers + private final AnnotationMirror MODIFIABLE; + private final AnnotationMirror GROW_SHRINK; + private final AnnotationMirror GROW_REPLACE; + private final AnnotationMirror SHRINK_REPLACE; + private final AnnotationMirror GROWABLE; + private final AnnotationMirror SHRINKABLE; + private final AnnotationMirror REPLACEABLE; + private final AnnotationMirror POLY_MODIFIABLE; + @SuppressWarnings("this-escape") public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -36,9 +55,23 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { // later. this.iteratorMethodElement = TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); + // Cache types + this.setType = getElementUtils().getTypeElement("java.util.Set").asType(); + this.mapEntryType = getElementUtils().getTypeElement("java.util.Map.Entry").asType(); + this.iteratorType = getElementUtils().getTypeElement("java.util.Iterator").asType(); + // Cache annotation mirrors for performance. this.UNKNOWN_MODIFIABILITY = AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); + this.MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Modifiable.class); + this.GROW_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), GrowShrink.class); + this.GROW_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), GrowReplace.class); + this.SHRINK_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), ShrinkReplace.class); + this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); + this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); + this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); + this.POLY_MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyModifiable.class); + addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_MODIFIABILITY); postInit(); } @@ -98,4 +131,88 @@ public ParameterizedExecutableType methodFromUse( return mType; } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator(new ModifiabilityTypeAnnotator(this), super.createTypeAnnotator()); + } + + private class ModifiabilityTypeAnnotator extends TypeAnnotator { + public ModifiabilityTypeAnnotator(ModifiabilityAnnotatedTypeFactory factory) { + super(factory); + } + + @Override + public Void visitDeclared(AnnotatedDeclaredType type, Void p) { + super.visitDeclared(type, p); + + // Skip if polymorphic or explicit bottom + if (type.hasPrimaryAnnotation(POLY_MODIFIABLE)) { + return null; + } + + TypeMirror underlyingType = type.getUnderlyingType(); + TypeMirror erasure = getProcessingEnv().getTypeUtils().erasure(underlyingType); + + if (getProcessingEnv() + .getTypeUtils() + .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(setType))) { + // Set: Drop R bit + removeReplaceable(type); + } else if (getProcessingEnv() + .getTypeUtils() + .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(mapEntryType))) { + // Map.Entry: Drop G and S bits + removeGrowable(type); + removeShrinkable(type); + } else if (getProcessingEnv() + .getTypeUtils() + .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(iteratorType))) { + // Iterator: Drop G and R bits + removeGrowable(type); + removeReplaceable(type); + } + + return null; + } + } + + // Helper to remove 'Grow' capability (bit 100) + private void removeGrowable(AnnotatedTypeMirror type) { + if (type.hasPrimaryAnnotation(GROWABLE)) { // 100 + type.replaceAnnotation(UNKNOWN_MODIFIABILITY); // 000 + } else if (type.hasPrimaryAnnotation(GROW_SHRINK)) { // 110 + type.replaceAnnotation(SHRINKABLE); // 010 + } else if (type.hasPrimaryAnnotation(GROW_REPLACE)) { // 101 + type.replaceAnnotation(REPLACEABLE); // 001 + } else if (type.hasPrimaryAnnotation(MODIFIABLE)) { // 111 + type.replaceAnnotation(SHRINK_REPLACE); // 011 + } + } + + // Helper to remove 'Shrink' capability (bit 010) + private void removeShrinkable(AnnotatedTypeMirror type) { + if (type.hasPrimaryAnnotation(SHRINKABLE)) { // 010 + type.replaceAnnotation(UNKNOWN_MODIFIABILITY); // 000 + } else if (type.hasPrimaryAnnotation(GROW_SHRINK)) { // 110 + type.replaceAnnotation(GROWABLE); // 100 + } else if (type.hasPrimaryAnnotation(SHRINK_REPLACE)) { // 011 + type.replaceAnnotation(REPLACEABLE); // 001 + } else if (type.hasPrimaryAnnotation(MODIFIABLE)) { // 111 + type.replaceAnnotation(GROW_REPLACE); // 101 + } + } + + // Helper to remove 'Replace' capability (bit 001) + private void removeReplaceable(AnnotatedTypeMirror type) { + if (type.hasPrimaryAnnotation(REPLACEABLE)) { // 001 + type.replaceAnnotation(UNKNOWN_MODIFIABILITY); // 000 + } else if (type.hasPrimaryAnnotation(GROW_REPLACE)) { // 101 + type.replaceAnnotation(GROWABLE); // 100 + } else if (type.hasPrimaryAnnotation(SHRINK_REPLACE)) { // 011 + type.replaceAnnotation(SHRINKABLE); // 010 + } else if (type.hasPrimaryAnnotation(MODIFIABLE)) { // 111 + type.replaceAnnotation(GROW_SHRINK); // 110 + } + } } diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java new file mode 100644 index 000000000000..aa738c356a3d --- /dev/null +++ b/checker/tests/modifiability/SetTest.java @@ -0,0 +1,140 @@ +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.GrowReplace; +import org.checkerframework.checker.modifiability.qual.GrowShrink; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.ShrinkReplace; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; + +class SetTest { + // ========================================================== + // Set: No Replace methods. Implicitly @Replaceable. + // ========================================================== + + void testSetAssignment( + @UnknownModifiability Set unknown, + @Replaceable Set replaceable, + @Shrinkable Set shrinkable, + @Modifiable Set modifiable, + @Growable Set growable, + @GrowReplace Set growReplace, + @ShrinkReplace Set shrinkReplace, + @GrowShrink Set growShrink) { + + // Unknown <-> Replaceable + @Replaceable Set s1 = unknown; // Valid: Unknown -> Replaceable (Implicit R) + @UnknownModifiability Set s2 = replaceable; // Valid: Replaceable <: Unknown + + // Shrinkable <-> ShrinkReplace + @ShrinkReplace Set s3 = shrinkable; // Valid: Shrinkable -> ShrinkReplace (Implicit R) + @Shrinkable Set s4 = shrinkReplace; // Valid: ShrinkReplace <: Shrinkable + + // GrowShrink <-> Modifiable + @Modifiable Set s5 = growShrink; // Valid: GrowShrink -> Modifiable (Implicit R) + @GrowShrink Set s6 = modifiable; // Valid: Modifiable <: GrowShrink + + // Growable <-> GrowReplace + @GrowReplace Set s7 = growable; // Valid: Growable -> GrowReplace (Implicit R) + @Growable Set s8 = growReplace; // Valid: GrowReplace <: Growable + } + + // ========================================================== + // Map.Entry: Drops Grow (100) and Shrink (010) capabilities. + // Effectively supports only Replace (001). + // ========================================================== + + void testEntryAssignment( + // Group 1: No Replace Bit (Result: @UnknownModifiability) + Map.@UnknownModifiability Entry unknown, + Map.@Growable Entry growable, + Map.@Shrinkable Entry shrinkable, + Map.@GrowShrink Entry growShrink, + + // Group 2: Has Replace Bit (Result: @Replaceable) + Map.@Replaceable Entry replaceable, + Map.@GrowReplace Entry growReplace, + Map.@ShrinkReplace Entry shrinkReplace, + Map.@Modifiable Entry modifiable) { + + // Group 1: Unknown = Growable = Shrinkable = GrowShrink -> + // @UnknownModifiability + // These types lose G and S capabilities, degrading to Unknown (000). + + Map.@UnknownModifiability Entry u1 = growable; + Map.@UnknownModifiability Entry u2 = shrinkable; + Map.@UnknownModifiability Entry u3 = growShrink; + + // Since they are all @Unknown (000), they can be assigned to each other + Map.@Growable Entry g1 = unknown; + Map.@Shrinkable Entry s1 = unknown; + Map.@GrowShrink Entry gs1 = unknown; + Map.@Growable Entry g2 = growShrink; + + // Group 2: Replaceable = GrowReplace = ShrinkReplace = Modifiable -> + // @Replaceable + // These types retain R (001) but lose G and S, degrading to Replaceable. + + Map.@Replaceable Entry r1 = growReplace; + Map.@Replaceable Entry r2 = shrinkReplace; + Map.@Replaceable Entry r3 = modifiable; + + // Since they are all @Replaceable (001), they can be assigned to each other + Map.@GrowReplace Entry gr1 = replaceable; + Map.@ShrinkReplace Entry sr1 = replaceable; + Map.@Modifiable Entry m1 = replaceable; + + Map.@ShrinkReplace Entry sr2 = growReplace; + Map.@GrowReplace Entry gr2 = modifiable; + } + + // ========================================================== + // Iterator: No Grow/Replace methods. Implicitly @GrowReplace. + // ========================================================== + + void testIteratorAssignment( + @UnknownModifiability Iterator unknown, + @Growable Iterator growable, + @Replaceable Iterator replaceable, + @GrowReplace Iterator growReplace, + @Shrinkable Iterator shrinkable, + @GrowShrink Iterator growShrink, + @ShrinkReplace Iterator shrinkReplace, + @Modifiable Iterator modifiable) { + + // Group 1: Unknown = Growable = Replaceable = GrowReplace + // All these imply @GrowReplace (1?1), so they are effectively equal and + // assignable. + + @GrowReplace Iterator gr1 = unknown; + @GrowReplace Iterator gr2 = growable; + @GrowReplace Iterator gr3 = replaceable; + @GrowReplace Iterator gr4 = growReplace; + + @UnknownModifiability Iterator u1 = growReplace; + @Growable Iterator g1 = growReplace; + @Replaceable Iterator r1 = growReplace; + + @Growable Iterator g2 = unknown; + @Replaceable Iterator r2 = growable; + + // Group 2: Shrinkable = GrowShrink = ShrinkReplace = Modifiable + // All these imply @Modifiable (111) because they have the S bit, + // and Iterator adds G and R bits. (010 + 101 = 111). + + @Modifiable Iterator m1 = shrinkable; + @Modifiable Iterator m2 = growShrink; + @Modifiable Iterator m3 = shrinkReplace; + @Modifiable Iterator m4 = modifiable; + + @Shrinkable Iterator s1 = modifiable; + @GrowShrink Iterator gs1 = modifiable; + @ShrinkReplace Iterator sr1 = modifiable; + + @GrowShrink Iterator gs2 = shrinkable; + @ShrinkReplace Iterator sr2 = growShrink; + } +} diff --git a/checker/tests/modifiability/WillThrowUOETest.java b/checker/tests/modifiability/WillThrowUOETest.java index dca783406e95..cbe438a7e4a7 100644 --- a/checker/tests/modifiability/WillThrowUOETest.java +++ b/checker/tests/modifiability/WillThrowUOETest.java @@ -18,7 +18,6 @@ void testSortedSet(SortedSet s) { void testImplementation(TreeSet ts) { // Since TreeSet implements SortedSet, it inherits the methods. - // The stub file annotates the interface SortedSet. // We verify that calling them on a concrete implementation also triggers the // warning. From ee646cbb72d0dc702e69ee847b708fad04b2471f Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 8 Feb 2026 17:38:12 -0800 Subject: [PATCH 036/308] edit the Modifiability checker maual to reflect the new type system --- docs/manual/modifiability-checker.tex | 216 ++++++++++++++++---------- 1 file changed, 136 insertions(+), 80 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 5a678398d3b0..775292ccaf6a 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -27,6 +27,13 @@ common class of runtime bugs where code compiles but fails at execution with \code{UnsupportedOperationException}. +The modifiability of an colletion is categorized into three orthogonal capabilities: \textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. +\begin{itemize} + \item \textbf{Grow (G)}: The collection can grow by adding new elements (add, addAll..). + \item \textbf{Shrink (S)}: The collection can shrink by removing existing elements (remove, removeAll, clear, retainAll..). + \item \textbf{Replace (R)}: The collection can replace existing elements with new values at specific locations/keys (set, replaceAll, setValue..). +\end{itemize} + To run the Modifiability Checker: \begin{Verbatim} @@ -38,16 +45,22 @@ \sectionAndLabel{Modifiability annotations}{modifiability-annotations} -The Modifiability Checker provides a small lattice of type qualifiers that -describe the modifiability capability of a collection. The main user-visible -qualifiers are: +The Modifiability Checker provides a lattice of type qualifiers that +describe the modifiability capability of a collection. The qualifiers +describe which mutating operations are permitted on the collection. + +The main user-visible qualifiers are: \begin{itemize} \item \code{@UnknownModifiability}, - \item \code{@Modifiable}, and - \item \code{@Unmodifiable}. + \item \code{@Modifiable}, + \item \code{@Unmodifiable}, + \item \code{@Growable}, + \item \code{@Shrinkable}, + \item \code{@Replaceable}, + \item \code{@GrowShrink}, + \item \code{@GrowReplace}, and + \item \code{@ShrinkReplace}. \end{itemize} -Internally, the checker also uses a bottom qualifier -\code{@BottomModifiable}, which programmers do not write. The qualifier hierarchy is shown in Figure~\ref{fig-modifiability-lattice}. @@ -56,13 +69,21 @@ % TODO: add a figure that follows the sturucture of other checker manuals. \begin{figure} \centering - \begin{minipage}{0.5\linewidth} + \begin{minipage}{0.8\linewidth} \begin{verbatim} - @UnknownModifiability - / \ -@Modifiable @Unmodifiable - \ / - @BottomModifiable + @UnknownModifiability + (@Unmodifiable) + / | \ + / | \ + @Growable @Shrinkable @Replaceable + | \ / \ / | + | x x | + | / \ / \ | + @GrowShrink @GrowReplace @ShrinkReplace + \ | / + \ | / + \ | / + @Modifiable \end{verbatim} \end{minipage} \caption{Modifiability qualifier hierarchy.} @@ -76,22 +97,59 @@ collection is modifiable or unmodifiable; it may be either. This is the default qualifier that is applied to unannotated collection types. +\item[\code{@Unmodifiable}] +An alias for \code{@UnknownModifiability}. Programmers can write \code{@Unmodifiable} +to indicate that a collection should not be modified, but the checker treats it +identically to \code{@UnknownModifiability}. + \item[\code{@Modifiable}] -The collection is known to support all standard mutating operations, such as -\code{add}, \code{remove}, \code{clear}, and \code{put}, without throwing -\code{UnsupportedOperationException}. A call to a mutating method is permitted -only when the receiver is annotated as \code{@Modifiable}. +Calling all mutating operations such as \code{add}, \code{remove}, \code{clear}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. -\item[\code{@Unmodifiable}] -The collection is known to be unmodifiable: any mutating operation would throw -\code{UnsupportedOperationException}. A call to a mutating method on a -\code{@Unmodifiable} receiver always yields a type-checking error. +\item[\code{@Growable}] +Calling grow operations such as \code{add}, \code{addAll}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. + +\item[\code{@Shrinkable}] +Calling shrink operations such as \code{remove}, \code{clear}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. + +\item[\code{@Replaceable}] +Calling replace operations such as \code{set}, \code{replaceAll}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. + +\item[\code{@GrowShrink}] +Calling grow and shrink operations on this collection will not result in throwing \code{UnsupportedOperationException}. + +\item[\code{@GrowReplace}] +Calling grow and replace operations on this collection will not result in throwing \code{UnsupportedOperationException}. + +\item[\code{@ShrinkReplace}] +Calling shrink and replace operations on this collection will not result in throwing \code{UnsupportedOperationException}. + +\end{description} + +\subsectionAndLabel{Types that don't have full grow, shrink, and replace}{types-that-dont-have-full-gsr} + +There are types that do not support some mutations. + +One example is \code{Set}. A \code{Set} can grow or shrink, but replacing an element in a set is not meaningful, so the \code{Set} interface does not have replace methods. This causes ambiguity when we annotate \code{Set} as \code{@Modifiable} vs \code{@GrowShrink}, since they mean the same thing for a \code{Set}. To solve this issue, the Modifiability Checker treats all \code{Set}s as not replaceable by default. +\begin{itemize} + \item If a \code{Set} is annotated as \code{@UnknownModifiability}, the checker respects it. + \item If a \code{Set} is annotated as \code{@GrowReplace}, the modifiability checker defaults it to \code{@Growable}. + \item If a \code{Set} is annotated as \code{@ShrinkReplace}, the modifiability checker defaults it to \code{@Shrinkable}. + \item If a \code{Set} is annotated as \code{@Modifiable}, the modifiability checker defaults it to \code{@GrowShrink}. +\end{itemize} + +Another example is \code{Map.Entry}. \code{Map.Entry} only has \code{setValue} as an optional method. To maintain consistency, the Modifiability Checker treats all \code{Map.Entry} objects as not growable and not shrinkable. + +Another example is \code{Iterator}. \code{Iterator} only has \code{remove} as an optional method, and growable and replaceable operations do not exist. To maintain consistency, the Modifiability Checker treats all iterators as not replaceable and not growable. Note that \code{ListIterator} is treated normally because it has \code{add} and \code{replace} operations. + +\subsectionAndLabel{Polymorphic Types}{modifiability-polymorphic} -\item[\code{@BottomModifiable}] -The bottom qualifier in the hierarchy. It is a subtype of both -\code{@Modifiable} and \code{@Unmodifiable}. It is used internally by the -checker and is not intended to be written by programmers. +Polymorphic annotations serve as method contracts ensuring that the output type retains specific capability bits from the input type (or receiver). +\begin{description} +\item[\code{@PolyModifiable}] + Full Preservation. Output capabilities (G, S, R) exactly match the Input. Used by strict wrappers like \code{Collections.synchronizedList}. +\item[\code{@PolyShrink}] + Shrink Preservation. Output retains the \textbf{S} bit of the Input. G and R are fixed or determined separately. \end{description} \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} @@ -114,86 +172,84 @@ % Each annotation’s Javadoc should include a precise description and an example % use, and should link back to this chapter using % \code{@checker\_framework.manual \#modifiability-checker Modifiability Checker}. -% Internal qualifiers such as \code{@BottomModifiable} should be documented last. \sectionAndLabel{What the Modifiability Checker checks}{modifiability-rules} The Modifiability Checker enforces that mutating methods are only called on -collections that are definitely modifiable. +collections that have the required modifiability capability. A \emph{mutating method} is any method that may change the size or contents of a -collection, such as: -\begin{itemize} - \item \code{add}, \code{addAll} - \item \code{remove}, \code{removeAll}, \code{retainAll}, \code{clear} - \item \code{put}, \code{putAll}, \code{replace}, \code{replaceAll} -\end{itemize} +collection. +When annotating a method signature, the \textbf{receiver} (\code{this}) must be annotated with the minimum capabilities (G, S, R bits) required to perform the operation. -A \emph{read-only method} is any method that does not modify the collection, -such as: \begin{itemize} - \item \code{contains}, \code{containsAll} - \item \code{equals}, \code{hashCode} - \item \code{get}, \code{getFirst}, \code{getLast} - \item \code{indexOf}, \code{isEmpty}, \code{toArray} -\end{itemize} + \item \textbf{Grow (G)}: If the method increases the size of the collection, the receiver requires \code{@Growable} (G=1). + Examples: \code{add}, \code{addAll}, \code{offer}, \code{addFirst}, \code{addLast}, \code{ListIterator.add}. -The checker enforces the following rules: + \item \textbf{Shrink (S)}: If the method decreases the size (or removes elements), the receiver requires \code{@Shrinkable} (S=1). + Examples: \code{remove}, \code{removeAll}, \code{retainAll}, \code{clear}, \code{poll}, \code{pop}, \code{Iterator.remove}. -\begin{itemize} - \item A call to a mutating method is permitted only if the receiver is - annotated as \code{@Modifiable}. - \item If the receiver is annotated as \code{@Unmodifiable}, the checker issues - an error because the call would always throw - \code{UnsupportedOperationException}. - \item If the receiver is annotated as \code{@UnknownModifiability}, the checker - conservatively issues a warning, because the collection might be - unmodifiable at run time. - \item Read-only methods may be called on collections of any modifiability - qualifier (\code{@UnknownModifiability}, \code{@Modifiable}, - \code{@Unmodifiable}) without error. - \item Subtyping follows the lattice in - Figure~\ref{fig-modifiability-lattice}: both \code{@Modifiable} and - \code{@Unmodifiable} are subtypes of \code{@UnknownModifiability}, and - \code{@BottomModifiable} is a subtype of both. -\end{itemize} + \item \textbf{Replace (R)}: If the method updates an existing element \emph{at a specific location/key}, the receiver requires \code{@Replaceable} (R=1). + Examples: \code{List.set}, \code{ListIterator.set}, \code{Map.replace}, \code{Map.replaceAll}, \code{Collections.sort}, \code{Map.Entry.setValue}. + + \item \textbf{Combined Requirements}: + \begin{itemize} + \item \code{Map.put(K, V)}: Can grow OR replace. Requirement: \code{@GrowReplace} (G=1, R=1). + \item \code{Map.compute(...)} / \code{merge(...)}: Can grow, shrink, or replace. Requirement: \code{@Modifiable} (G=1, S=1, R=1). + \end{itemize} -The checker does not rely on run-time tests or exceptions; it reasons entirely -from static types. Methods that are known to return unmodifiable collections -(such as \code{List.of()} and \code{Collections.unmodifiableList(...)}) should -be annotated in the JDK as returning \code{@Unmodifiable}, so that the checker -can track modifiability through library calls. + \item \textbf{Read-Only Operations}: If the method does not modify the structure or elements of the collection, no specific capabilities are required. The receiver can be \code{@UnknownModifiability}. + Examples: \code{get}, \code{contains}, \code{size}, \code{isEmpty}. +\end{itemize} \sectionAndLabel{Examples}{modifiability-examples} \begin{Verbatim} import java.util.*; +import org.checkerframework.checker.modifiability.qual.*; class Demo { - void factoryExamples() { - @Modifiable List a = new ArrayList<>(); - @Unmodifiable List b = List.of("x", "y"); - @UnknownModifiability List c = maybeList(); - - a.add("z"); // OK: receiver is @Modifiable + void basicUsage() { + @Modifiable List mod = new ArrayList<>(); + mod.add("a"); // OK: @Modifiable implies G=1, S=1, R=1 + mod.remove("a"); // OK + mod.set(0, "b"); // OK - b.add("z"); // error: mutating @Unmodifiable list + @Unmodifiable List unmod = List.of("a", "b"); + unmod.add("c"); // Error: receiver requires @Growable, found @Unmodifiable + unmod.get(0); // OK: read-only access matches any capability + } - c.add("z"); // error: warning! possible mutation of a collection whose - // modifiability is unknown + void fineGrainedPermissions(@Growable List g, + @Shrinkable List s, + @Replaceable List r) { + // Growable list allows adding elements (G=1) + g.add("a"); // OK + g.remove("a"); // Error: remove requires @Shrinkable + g.set(0, "b"); // Error: set requires @Replaceable + + // Shrinkable list allows removing elements (S=1) + s.remove("a"); // OK + s.add("a"); // Error: add requires @Growable + + // Replaceable list allows updating elements (R=1) + r.set(0, "b"); // OK + r.add("b"); // Error: add requires @Growable } - @UnknownModifiability List maybeList() { - // Might return modifiable or unmodifiable list. - return Math.random() > 0.5 ? List.of("a") : new ArrayList<>(); + void combinedPermissions(@GrowReplace Map map) { + // Map.put requires both Grow and Replace capabilities (G=1, R=1) + map.put("key", "value"); // OK: receiver is @GrowReplace + + // Map.remove requires Shrink capability (S=1) + map.remove("key"); // Error: receiver is @GrowReplace, + // but remove requires @Shrinkable } - void readonlyExamples(@UnknownModifiability List x, - @Unmodifiable List y) { - x.contains("a"); // OK - y.get(0); // OK - // Read-only methods are safe for any qualifier. + // Polymorphism preserves capabilities + @PolyModifiable List process(@PolyModifiable List list) { + return list.subList(0, 1); } } \end{Verbatim} \ No newline at end of file From 1cc84e8c534463a7211a2eada1dd0c3196b0f5f2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 11 Feb 2026 15:20:56 -0800 Subject: [PATCH 037/308] change queue and the subset of it from modifiable to growshrink except LinkedList --- .../ModifiabilityAnnotatedTypeFactory.java | 16 +++++++++++++--- checker/tests/modifiability/SetTest.java | 9 +++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index efac421d6407..bf9d9c5a7b88 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -35,6 +35,8 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory // Cached Types for default handling private final TypeMirror setType; + private final TypeMirror queueType; + private final TypeMirror linkedListType; private final TypeMirror mapEntryType; private final TypeMirror iteratorType; @@ -57,6 +59,8 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); // Cache types this.setType = getElementUtils().getTypeElement("java.util.Set").asType(); + this.queueType = getElementUtils().getTypeElement("java.util.Queue").asType(); + this.linkedListType = getElementUtils().getTypeElement("java.util.LinkedList").asType(); this.mapEntryType = getElementUtils().getTypeElement("java.util.Map.Entry").asType(); this.iteratorType = getElementUtils().getTypeElement("java.util.Iterator").asType(); @@ -155,9 +159,15 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { TypeMirror erasure = getProcessingEnv().getTypeUtils().erasure(underlyingType); if (getProcessingEnv() - .getTypeUtils() - .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(setType))) { - // Set: Drop R bit + .getTypeUtils() + .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(setType)) + || (getProcessingEnv() + .getTypeUtils() + .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(queueType)) + && !getProcessingEnv() + .getTypeUtils() + .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(linkedListType)))) { + // Set or Queue (but not LinkedList): Drop R bit removeReplaceable(type); } else if (getProcessingEnv() .getTypeUtils() diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index aa738c356a3d..6d1ee84c90e5 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -1,5 +1,6 @@ import java.util.Iterator; import java.util.Map; +import java.util.Queue; import java.util.Set; import org.checkerframework.checker.modifiability.qual.GrowReplace; import org.checkerframework.checker.modifiability.qual.GrowShrink; @@ -42,6 +43,14 @@ void testSetAssignment( @Growable Set s8 = growReplace; // Valid: GrowReplace <: Growable } + void testQueueAssignment( + @Modifiable Queue modifiable, @GrowShrink Queue growShrink) { + + // GrowShrink <-> Modifiable + @Modifiable Queue q5 = growShrink; // Valid: GrowShrink -> Modifiable (Implicit R) + @GrowShrink Queue q6 = modifiable; // Valid: Modifiable <: GrowShrink + } + // ========================================================== // Map.Entry: Drops Grow (100) and Shrink (010) capabilities. // Effectively supports only Replace (001). From 008fcc91421a88e33fa6ebb8e966e92d520679d1 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 16 Feb 2026 09:00:57 -0800 Subject: [PATCH 038/308] Code review changes --- .../modifiability/qual/Modifiable.java | 4 +- .../modifiability/qual/PolyModifiable.java | 7 +- .../modifiability/qual/PolyShrink.java | 4 +- .../{WillThrowUOE.java => ThrowsUOE.java} | 7 +- .../qual/UnknownModifiability.java | 7 +- .../modifiability/qual/Unmodifiable.java | 4 +- .../ModifiabilityAnnotatedTypeFactory.java | 73 ++++--- .../modifiability/ModifiabilityChecker.java | 3 +- .../modifiability/ModifiabilityVisitor.java | 16 +- .../checker/modifiability/messages.properties | 2 +- ...llThrowUOETest.java => ThrowsUOETest.java} | 30 +-- docs/manual/manual-style.tex | 4 +- docs/manual/modifiability-checker.tex | 185 +++++++++++------- 13 files changed, 210 insertions(+), 136 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{WillThrowUOE.java => ThrowsUOE.java} (71%) rename checker/tests/modifiability/{WillThrowUOETest.java => ThrowsUOETest.java} (67%) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 07223188453f..2bff5ec0635f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -8,9 +8,11 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * Calling all mutating operations such as {@code add}, {@code remove}, {@code clear}, etc. on this + * Calling mutating operations such as {@code add}, {@code remove}, {@code clear}, etc. on this * collection will not result in throwing {@link UnsupportedOperationException}. * + *

MDE: Mention the non-guarantee, as is done for other qualifiers. + * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index ed6a0e4c859c..0acaef56339e 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -7,8 +7,8 @@ /** * A polymorphic qualifier for modifiability. * - *

Use on methods that preserve or transfer modifiability — for example, {@code List.subList()}, - * {@code iterator()}, or {@code stream()}. + *

Use on methods that preserve or transfer modifiability — for example, {@code + * List.subList()}, {@code iterator()}, or {@code stream()}. * *

The same qualifier that appears on the receiver will also appear on the return type (and * possibly parameters). For example: @@ -21,6 +21,9 @@ * * If the receiver is {@code @Unmodifiable}, the return is {@code @Unmodifiable}. If the receiver is * {@code @Modifiable}, the return is {@code @Modifiable}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ @PolymorphicQualifier(UnknownModifiability.class) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java index 6d5d9f5b036b..c73d29d0590b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java @@ -10,10 +10,12 @@ /** * A polymorphic qualifier for modifiability that preserves the shrink capability. * - *

Use on methods that preserve shrinkability — for example, {@code Map.keySet()}. + *

Use on methods that preserve shrinkability — for example, {@code Map.keySet()}. * * @checker_framework.manual #modifiability-checker Modifiability Checker *

TODO: need to implement + * @checker_framework.manual #modifiability-checker Modifiability Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java similarity index 71% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java index 9a3341ee81e8..3651620510c7 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/WillThrowUOE.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java @@ -7,13 +7,12 @@ import java.lang.annotation.Target; /** - * Indicates that this method will always throw {@link UnsupportedOperationException} when called. - * - *

This is a declaration annotation. + * Indicates that the annotated method always throws {@link UnsupportedOperationException} when + * called. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -public @interface WillThrowUOE {} +public @interface ThrowsUOE {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java index 4be6c33d1316..e84e6cdb0cc0 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java @@ -10,10 +10,11 @@ /** * Represents an unknown or arbitrary modifiability capability. The checker cannot determine whether - * the collection is growable, shrinkable, or replaceable. This is the top qualifier in the - * hierarchy. + * the collection is growable, shrinkable, or replaceable. The Modifiability Checker issues an error + * if code calls any modification operation on a collection with this type. * - *

This is the default qualifier for unannotated types. + *

This is the default qualifier for unannotated types. This is the top qualifier in the + * hierarchy. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 3d2799a7288f..0152cdc84bdf 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -7,8 +7,8 @@ import java.lang.annotation.Target; /** - * The collection is known to be unmodifiable: calling a mutating method such as {@code add} or - * {@code remove} will always throw {@link UnsupportedOperationException}. + * The collection is known to be unmodifiable. Calling a mutating (growing, shrinking, or replacing) + * method such as {@code add} or {@code remove} always throws {@link UnsupportedOperationException}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index bf9d9c5a7b88..bb4088f4424f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -8,6 +8,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.qual.GrowReplace; import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; @@ -28,26 +29,52 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; +/** The type factory for the Modifiability Checker. */ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + /** The {@code Collection.iterator()} method. */ private final ExecutableElement iteratorMethodElement; - private final AnnotationMirror UNKNOWN_MODIFIABILITY; - // Cached Types for default handling + /** The {@code java.util.Set} type. */ private final TypeMirror setType; + + /** The {@code java.util.Queue} type. */ private final TypeMirror queueType; + + /** The {@code java.util.LinkedList} type. */ private final TypeMirror linkedListType; + + /** The {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryType; + + /** The {@code java.util.Iterator} type. */ private final TypeMirror iteratorType; - // Cached Qualifiers + /** The {@code @}{@link UnknknownModifiability} qualifier. */ + private final AnnotationMirror UNKNOWN_MODIFIABILITY; + + /** The {@code @}{@link Modifiable} qualifier. */ private final AnnotationMirror MODIFIABLE; + + /** The {@code @}{@link GrowShrink} qualifier. */ private final AnnotationMirror GROW_SHRINK; + + /** The {@code @}{@link GrowReplace} qualifier. */ private final AnnotationMirror GROW_REPLACE; + + /** The {@code @}{@link ShrinkReplace} qualifier. */ private final AnnotationMirror SHRINK_REPLACE; + + /** The {@code @}{@link ShrinkReplace} qualifier. */ private final AnnotationMirror GROWABLE; + + /** The {@code @}{@link Growable} qualifier. */ private final AnnotationMirror SHRINKABLE; + + /** The {@code @}{@link Replaceable} qualifier. */ private final AnnotationMirror REPLACEABLE; + + /** The {@code @}{@link PolyModifiable} qualifier. */ private final AnnotationMirror POLY_MODIFIABLE; @SuppressWarnings("this-escape") @@ -101,6 +128,8 @@ protected Set> createSupportedTypeQualifiers() { *

If a collection is {@code @Unmodifiable}, its iterator should also be treated as * {@code @Unmodifiable} (meaning {@code remove()} cannot be called). * + *

MDE: The below looks wrong. UnknownModifiability does not permit any mutations. + * *

If a collection is {@code @Modifiable}, its iterator is {@code @UnknownModifiability} (the * default behavior), which still permits calling {@code remove()}. */ @@ -114,6 +143,7 @@ public ParameterizedExecutableType methodFromUse( ParameterizedExecutableType mType = super.methodFromUse(tree, methodElt, receiverType, inferTypeArgs); + // MDE: Explain why using a @Poly qualifier does not work. // Special handling for the iterator() method. // We want the modifiability of the Iterator to match the modifiability of the // Collection. @@ -122,11 +152,11 @@ public ParameterizedExecutableType methodFromUse( AnnotatedTypeMirror returnType = mType.executableType.getReturnType(); if (receiverType.hasPrimaryAnnotation(Unmodifiable.class)) { - // collection.iterator() on an @Unmodifiable collection returns an @Unmodifiable + // Collection.iterator() on an @Unmodifiable collection returns an @Unmodifiable // Iterator. returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); } else if (receiverType.hasPrimaryAnnotation(Modifiable.class)) { - // collection.iterator() on a @Modifiable collection returns an + // Collection.iterator() on a @Modifiable collection returns an // @UnknownModifiability Iterator // (defaulting to bottom in this hierarchy is treated as safe for use). returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); @@ -150,34 +180,29 @@ public ModifiabilityTypeAnnotator(ModifiabilityAnnotatedTypeFactory factory) { public Void visitDeclared(AnnotatedDeclaredType type, Void p) { super.visitDeclared(type, p); - // Skip if polymorphic or explicit bottom + // MDE: Does this `if` statement handle explicit bottom? The comment says it does. + // Skip if polymorphic or explicit bottom. if (type.hasPrimaryAnnotation(POLY_MODIFIABLE)) { return null; } TypeMirror underlyingType = type.getUnderlyingType(); - TypeMirror erasure = getProcessingEnv().getTypeUtils().erasure(underlyingType); - - if (getProcessingEnv() - .getTypeUtils() - .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(setType)) - || (getProcessingEnv() - .getTypeUtils() - .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(queueType)) - && !getProcessingEnv() - .getTypeUtils() - .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(linkedListType)))) { + Types types = getProcessingEnv().getTypeUtils(); + TypeMirror erasure = types.erasure(underlyingType); + + // MDE: Rather than inefficiently and verbosely calling (e.g.) `types.erasure(setType)` + // every time that `visitDeclared` is called, that value should be stored as a field of the + // class. + if (types.isSubtype(erasure, types.erasure(setType)) + || (types.isSubtype(erasure, types.erasure(queueType)) + && !types.isSubtype(erasure, types.erasure(linkedListType)))) { // Set or Queue (but not LinkedList): Drop R bit removeReplaceable(type); - } else if (getProcessingEnv() - .getTypeUtils() - .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(mapEntryType))) { + } else if (types.isSubtype(erasure, types.erasure(mapEntryType))) { // Map.Entry: Drop G and S bits removeGrowable(type); removeShrinkable(type); - } else if (getProcessingEnv() - .getTypeUtils() - .isSubtype(erasure, getProcessingEnv().getTypeUtils().erasure(iteratorType))) { + } else if (types.isSubtype(erasure, types.erasure(iteratorType))) { // Iterator: Drop G and R bits removeGrowable(type); removeReplaceable(type); @@ -187,6 +212,8 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { } } + // The text about "bits" comes out of the blue. It should have documentation, or it should + // cross-reference documentation. // Helper to remove 'Grow' capability (bit 100) private void removeGrowable(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(GROWABLE)) { // 100 diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index df8d0100a434..83a001b1ef38 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -10,8 +10,7 @@ /** * A type-checker that warns, at compile time, if a program might throw {@link - * UnsupportedOperationException} at run time due to calling a mutating method on an unmodifiable - * collection. + * UnsupportedOperationException} at run time due to calling a mutating method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Modifiable} collections can * be safely mutated and {@code @Unmodifiable} collections cannot be mutated without risking an diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index d0f9bd58c77c..284c3e2a2632 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -2,7 +2,7 @@ import com.sun.source.tree.MethodInvocationTree; import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.modifiability.qual.WillThrowUOE; +import org.checkerframework.checker.modifiability.qual.ThrowsUOE; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -12,6 +12,8 @@ /** Visitor for the {@link ModifiabilityChecker}. */ public class ModifiabilityVisitor extends BaseTypeVisitor { + /** Create a visitor for the Modifiability Checker. */ + // MDE: Why is the type of `checker` BaseTypeChecker rather than ModifiabilityChecker? public ModifiabilityVisitor(BaseTypeChecker checker) { super(checker); } @@ -19,16 +21,18 @@ public ModifiabilityVisitor(BaseTypeChecker checker) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void p) { ExecutableElement method = TreeUtils.elementFromUse(node); - // Check if the method being invoked is annotated with @WillThrowUOE. + // Check if the method being invoked is annotated with @ThrowsUOE. // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw - // UnsupportedOperationException at runtime, so we report an error immediately. - if (atypeFactory.getDeclAnnotation(method, WillThrowUOE.class) != null) { - checker.reportError(node, "usage.will.throw.uoe", method.getSimpleName()); + // UnsupportedOperationException at run time, so we report an error immediately. + if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { + checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); } return super.visitMethodInvocation(node, p); } - // Suppress the framework’s “constructor result must be TOP” check. + // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they + // exist.) + // Suppress the framework's "constructor result must be TOP" check. // For Modifiability, constructors may legitimately produce @Modifiable. @Override protected void checkConstructorResult( diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index be9d69334519..7eda6bdc0bf3 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1 +1 @@ -usage.will.throw.uoe=The method %s is annotated with @WillThrowUOE and will throw an UnsupportedOperationException if called. +usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. diff --git a/checker/tests/modifiability/WillThrowUOETest.java b/checker/tests/modifiability/ThrowsUOETest.java similarity index 67% rename from checker/tests/modifiability/WillThrowUOETest.java rename to checker/tests/modifiability/ThrowsUOETest.java index cbe438a7e4a7..f6fc2cb26656 100644 --- a/checker/tests/modifiability/WillThrowUOETest.java +++ b/checker/tests/modifiability/ThrowsUOETest.java @@ -6,13 +6,13 @@ import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; -public class WillThrowUOETest { +public class ThrowsUOETest { void testSortedSet(SortedSet s) { - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) s.addFirst("foo"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) s.addLast("bar"); } @@ -21,50 +21,50 @@ void testImplementation(TreeSet ts) { // We verify that calling them on a concrete implementation also triggers the // warning. - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) ts.addFirst("foo"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) ts.addLast("bar"); } void testSortedMap(SortedMap m) { - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putFirst("foo", "bar"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putLast("baz", "qux"); } void testImplementation(TreeMap tm) { - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) tm.putFirst("foo", "bar"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) tm.putLast("baz", "qux"); } void testNavigableMap(NavigableMap m) { - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putFirst("foo", "bar"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putLast("baz", "qux"); } void testConcurrentNavigableMap(ConcurrentNavigableMap m) { - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putFirst("foo", "bar"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putLast("baz", "qux"); } void testConcurrentSkipListMap(ConcurrentSkipListMap m) { - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putFirst("foo", "bar"); - // :: error: (usage.will.throw.uoe) + // :: error: (usage.throws.uoe) m.putLast("baz", "qux"); } } diff --git a/docs/manual/manual-style.tex b/docs/manual/manual-style.tex index db3510d1fc6e..119e65070c47 100644 --- a/docs/manual/manual-style.tex +++ b/docs/manual/manual-style.tex @@ -174,9 +174,9 @@ % Arg 1: .html reference, without the .../api/ prefix % Arg 2: What will appear in the formatted manual. % Do not use \url in the body of any macro -\newcommand{\sunjavadoc}[2]{\href{https://docs.oracle.com/en/java/javase/17/docs/api/#1}{\<#2>}} +\newcommand{\sunjavadoc}[2]{\href{https://docs.oracle.com/en/java/javase/25/docs/api/#1}{\<#2>}} % Like \sunjavadoc, but for annotations and prepends "@". -\newcommand{\sunjavadocanno}[2]{\href{https://docs.oracle.com/en/java/javase/17/docs/api/#1}{\<@#2>}} +\newcommand{\sunjavadocanno}[2]{\href{https://docs.oracle.com/en/java/javase/25/docs/api/#1}{\<@#2>}} % Now, for Java EE (version 7 is the last version): \newcommand{\javaeejavadoc}[2]{\href{https://docs.oracle.com/javaee/7/api/#1}{\<#2>}} \newcommand{\javaeejavadocanno}[2]{\href{https://docs.oracle.com/javaee/7/api/#1}{\<@#2>}} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 775292ccaf6a..f84586c77b68 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -3,17 +3,33 @@ The Modifiability Checker issues a warning, at compile time, if a program might throw \code{UnsupportedOperationException} at run time due to invoking a mutating method (such as \code{add} or \code{remove}) on an unmodifiable -collection. +collection, map, or iterator. +(This chapter generally uses just ``collection'', for brevity.) + +% MDE: TODO +In Java, an \emp{optional} method is one An \emph{unmodifiable collection} is one that throws \code{UnsupportedOperationException} if a mutating method is called. The JDK provides many such collections, for example, the results of: \begin{itemize} - \item \code{List.of()}, \code{List.copyOf()} - \item \code{Set.of()}, \code{Set.copyOf()} - \item \code{Map.of()}, \code{Map.copyOf()} - \item \code{Collections.emptyList()}, \code{Collections.emptySet()}, \code{Collections.emptyMap()} - \item \code{Collections.unmodifiableList()}, \code{Collections.unmodifiableSet()}, \code{Collections.unmodifiableMap()} +\item + \sunjavadoc{java.base/java/util/List.html\#of(E...)}{\code{List.of()}}, + \sunjavadoc{java.base/java/util/List.html\#copyOf(java.util.Collection)}{\code{List.copyOf()}} +\item + \sunjavadoc{java.base/java/util/Set.html\#of(E...)}{\code{Set.of()}}, + \sunjavadoc{java.base/java/util/Set.html\#copyOf(java.util.Collection)}{\code{Set.copyOf()}} +\item + \sunjavadoc{java.base/java/util/Map.html\#of(K,V,K,V)}{\code{Map.of()}}, + \sunjavadoc{java.base/java/util/Map.html\#copyOf(java.util.Map)}{\code{Map.copyOf()}} +\item + \sunjavadoc{java/util/Collections.html\#emptyList()}{\code{Collections.emptyList()}} + \sunjavadoc{java/util/Collections.html\#emptySet()}{\code{Collections.emptySet()}} + \sunjavadoc{java/util/Collections.html\#emptyMap()}{\code{Collections.emptyMap()}} +\item + \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableList(java.util.List)}{\code{Collections.unmodifiableList()}} + \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableSet(java.util.Set)}{\code{Collections.unmodifiableSet()}} + \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableMap(java.util.Map)}{\code{Collections.unmodifiableMap()}} \end{itemize} A \emph{modifiable collection} is one on which mutating methods can be called @@ -21,17 +37,16 @@ as \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be called on any collection, whether modifiable or unmodifiable. -The Modifiability Checker classifies each collection (and iterator) value by its -\emph{modifiability capability}. It guarantees that mutating methods are only -called on collections that are known to be modifiable. Thus, it prevents a -common class of runtime bugs where code compiles but fails at execution with -\code{UnsupportedOperationException}. - -The modifiability of an colletion is categorized into three orthogonal capabilities: \textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. +A collection can be modified in three different ways: +\textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} - \item \textbf{Grow (G)}: The collection can grow by adding new elements (add, addAll..). - \item \textbf{Shrink (S)}: The collection can shrink by removing existing elements (remove, removeAll, clear, retainAll..). - \item \textbf{Replace (R)}: The collection can replace existing elements with new values at specific locations/keys (set, replaceAll, setValue..). + \item \textbf{Grow (G)}: The collection can grow by adding new elements + (\, \, \ldots). + \item \textbf{Shrink (S)}: The collection can shrink by removing + existing elements (\, \, \, \, \ldots). + \item \textbf{Replace (R)}: The collection can replace existing + elements with new values at specific locations/keys (\, \, + \, \ldots). \end{itemize} To run the Modifiability Checker: @@ -40,31 +55,11 @@ javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java \end{Verbatim} -You can also run it together with other checkers by listing multiple processors -to \code{-processor}. \sectionAndLabel{Modifiability annotations}{modifiability-annotations} -The Modifiability Checker provides a lattice of type qualifiers that -describe the modifiability capability of a collection. The qualifiers -describe which mutating operations are permitted on the collection. - -The main user-visible qualifiers are: -\begin{itemize} - \item \code{@UnknownModifiability}, - \item \code{@Modifiable}, - \item \code{@Unmodifiable}, - \item \code{@Growable}, - \item \code{@Shrinkable}, - \item \code{@Replaceable}, - \item \code{@GrowShrink}, - \item \code{@GrowReplace}, and - \item \code{@ShrinkReplace}. -\end{itemize} - -The qualifier hierarchy is shown in -Figure~\ref{fig-modifiability-lattice}. - +Figure~\ref{fig-modifiability-lattice} shows the type qualifiers of the +Modifiability Checker. % TODO: add a figure that follows the sturucture of other checker manuals. \begin{figure} @@ -92,55 +87,90 @@ \begin{description} -\item[\code{@UnknownModifiability}] -The top qualifier in the hierarchy. The checker does not know whether the -collection is modifiable or unmodifiable; it may be either. This is the default +\item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] +The checker does not know whether the +collection is modifiable or unmodifiable; it might be either. +The top qualifier in the hierarchy, and it is the default qualifier that is applied to unannotated collection types. -\item[\code{@Unmodifiable}] -An alias for \code{@UnknownModifiability}. Programmers can write \code{@Unmodifiable} -to indicate that a collection should not be modified, but the checker treats it -identically to \code{@UnknownModifiability}. - -\item[\code{@Modifiable}] -Calling all mutating operations such as \code{add}, \code{remove}, \code{clear}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. +\item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] +The collection is definitely unmodifiable. The checker treats it +identically to \code{@UnknownModifiability}, but the different +name is useful for documentation purposes. -\item[\code{@Growable}] -Calling grow operations such as \code{add}, \code{addAll}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. - -\item[\code{@Shrinkable}] -Calling shrink operations such as \code{remove}, \code{clear}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. - -\item[\code{@Replaceable}] -Calling replace operations such as \code{set}, \code{replaceAll}, etc. on this collection will not result in throwing \code{UnsupportedOperationException}. +\item[\refqualclass{checker/modifiability/qual}{Modifiable}] +Calling mutating operations such as \code{add}, \code{remove}, +\code{clear}, etc. on this collection will not result in throwing +\code{UnsupportedOperationException}. -\item[\code{@GrowShrink}] -Calling grow and shrink operations on this collection will not result in throwing \code{UnsupportedOperationException}. +\item[\refqualclass{checker/modifiability/qual}{Growable}] +Calling grow operations such as \code{add}, \code{addAll}, etc. on this +collection will not result in throwing +\code{UnsupportedOperationException}. +Calling shrink or replace operations might or might not result in +throwing \code{UnsupportedOperationException}. -\item[\code{@GrowReplace}] -Calling grow and replace operations on this collection will not result in throwing \code{UnsupportedOperationException}. +\item[\refqualclass{checker/modifiability/qual}{Shrinkable}] +Calling shrink operations such as \code{remove}, \code{clear}, etc. on this +collection will not result in throwing +\code{UnsupportedOperationException}. +Calling grow or replace operations might or might not result in +throwing \code{UnsupportedOperationException}. -\item[\code{@ShrinkReplace}] -Calling shrink and replace operations on this collection will not result in throwing \code{UnsupportedOperationException}. +\item[\refqualclass{checker/modifiability/qual}{Replaceable}] +Calling replace operations such as \code{set}, \code{replaceAll}, etc. on +this collection will not result in throwing +\code{UnsupportedOperationException}. +Calling grow or shrink operations might or might not result in +throwing \code{UnsupportedOperationException}. + +\item[\refqualclass{checker/modifiability/qual}{GrowShrink}] +Calling grow and shrink operations on this collection will not result in +throwing \code{UnsupportedOperationException}. +Calling replace operations might or might not result in +throwing \code{UnsupportedOperationException}. + +\item[\refqualclass{checker/modifiability/qual}{GrowReplace}] +Calling grow and replace operations on this collection will not result in +throwing \code{UnsupportedOperationException}. +Calling shrink operations might or might not result in +throwing \code{UnsupportedOperationException}. + +\item[\refqualclass{checker/modifiability/qual}{ShrinkReplace}] +Calling shrink and replace operations on this collection will not result in +throwing \code{UnsupportedOperationException}. +Calling grow operations might or might not result in +throwing \code{UnsupportedOperationException}. \end{description} -\subsectionAndLabel{Types that don't have full grow, shrink, and replace}{types-that-dont-have-full-gsr} +\subsectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} -There are types that do not support some mutations. +There are types that have no methods for certain mutations. -One example is \code{Set}. A \code{Set} can grow or shrink, but replacing an element in a set is not meaningful, so the \code{Set} interface does not have replace methods. This causes ambiguity when we annotate \code{Set} as \code{@Modifiable} vs \code{@GrowShrink}, since they mean the same thing for a \code{Set}. To solve this issue, the Modifiability Checker treats all \code{Set}s as not replaceable by default. +One example is \code{Set}. +The \code{Set} interface does not have replace methods, so +there is no difference between a replaceable \ and an unreplaceable \. +Likewise, \code{@Modifiable Set} and \code{@GrowShrink Set} mean the same +thing. +For consistency, the Modifiability Checker treats all \code{Set}s as not replaceable. \begin{itemize} - \item If a \code{Set} is annotated as \code{@UnknownModifiability}, the checker respects it. - \item If a \code{Set} is annotated as \code{@GrowReplace}, the modifiability checker defaults it to \code{@Growable}. - \item If a \code{Set} is annotated as \code{@ShrinkReplace}, the modifiability checker defaults it to \code{@Shrinkable}. - \item If a \code{Set} is annotated as \code{@Modifiable}, the modifiability checker defaults it to \code{@GrowShrink}. + \item If a \code{Set} is annotated as \code{@Modifiable}, the + Modifiability Checker treats it as \code{@GrowShrink}. + \item If a \code{Set} is annotated as \code{@GrowReplace}, the + Modifiability Checker changes it to \code{@Growable}. + \item If a \code{Set} is annotated as \code{@ShrinkReplace}, the + Modifiability Checker changes it to \code{@Shrinkable}. + \item If a \code{Set} is annotated as \code{@UnknownModifiability}, the + checker still reats it as \code{@UnknownModifiability} (no change). \end{itemize} -Another example is \code{Map.Entry}. \code{Map.Entry} only has \code{setValue} as an optional method. To maintain consistency, the Modifiability Checker treats all \code{Map.Entry} objects as not growable and not shrinkable. - Another example is \code{Iterator}. \code{Iterator} only has \code{remove} as an optional method, and growable and replaceable operations do not exist. To maintain consistency, the Modifiability Checker treats all iterators as not replaceable and not growable. Note that \code{ListIterator} is treated normally because it has \code{add} and \code{replace} operations. +Another example is \code{Map.Entry}. \code{Map.Entry} only has +\code{setValue} as an optional method. +To maintain consistency, the Modifiability Checker treats all \code{Map.Entry} objects as not growable and not shrinkable. + \subsectionAndLabel{Polymorphic Types}{modifiability-polymorphic} Polymorphic annotations serve as method contracts ensuring that the output type retains specific capability bits from the input type (or receiver). @@ -160,11 +190,11 @@ \begin{description} -\item[\code{@WillThrowUOE}] +\item[\refqualclass{checker/modifiability/qual}{ThrowsUOE}] This is a declaration annotation (not a type qualifier) that is applied to method and constructor declarations. It indicates that the method always throws \code{UnsupportedOperationException}. The Modifiability Checker issues an -error for any call to a method annotated with \code{@WillThrowUOE}. +error for any call to a method annotated with \code{@ThrowsUOE}. \end{description} @@ -252,4 +282,11 @@ return list.subList(0, 1); } } -\end{Verbatim} \ No newline at end of file +\end{Verbatim} + +% LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr +% LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap +% LocalWords: toArray addAll removeAll retainAll replaceAll setValue +% LocalWords: UnknownModifiability Growable GrowShrink GrowReplace +% LocalWords: ShrinkReplace growable ListIterator PolyModifiable addFirst +% LocalWords: synchronizedList PolyShrink ThrowsUOE addLast From bfa24486bb9e7dc0af2a3812018ac69df740fe12 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 16 Feb 2026 16:38:50 -0800 Subject: [PATCH 039/308] Code review edits --- docs/manual/modifiability-checker.tex | 169 +++++++++++++++----------- 1 file changed, 99 insertions(+), 70 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index f84586c77b68..d823f17b8fd2 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -1,13 +1,16 @@ \chapterAndLabel{Modifiability Checker}{modifiability-checker} -The Modifiability Checker issues a warning, at compile time, if a program might -throw \code{UnsupportedOperationException} at run time due to invoking a -mutating method (such as \code{add} or \code{remove}) on an unmodifiable -collection, map, or iterator. -(This chapter generally uses just ``collection'', for brevity.) - -% MDE: TODO -In Java, an \emp{optional} method is one +In Java, an \emp{optional} method is one that a class may or may not +support. If the subclass does not support the method, then calling the +method throws \. Thus, whenever a client +calls an optional method, there is a possibility of an exception. The +Modifiability Checker eliminates that possibility. + +If the Modifiability Checker issues no warning, then your program is +guaranteed not to throw \code{UnsupportedOperationException} due to +invoking a mutating method (such as \code{add} or \code{remove}) on an +unmodifiable collection, map, or iterator. (This chapter generally uses +just ``collection'', for brevity.) An \emph{unmodifiable collection} is one that throws \code{UnsupportedOperationException} if a mutating method is called. @@ -40,11 +43,11 @@ A collection can be modified in three different ways: \textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} - \item \textbf{Grow (G)}: The collection can grow by adding new elements + \item \textbf{Grow}: The collection can grow by adding new elements (\, \, \ldots). - \item \textbf{Shrink (S)}: The collection can shrink by removing + \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \ldots). - \item \textbf{Replace (R)}: The collection can replace existing + \item \textbf{Replace}: The collection can replace existing elements with new values at specific locations/keys (\, \, \, \ldots). \end{itemize} @@ -150,8 +153,8 @@ One example is \code{Set}. The \code{Set} interface does not have replace methods, so -there is no difference between a replaceable \ and an unreplaceable \. -Likewise, \code{@Modifiable Set} and \code{@GrowShrink Set} mean the same +there is no difference between a replaceable \ and an unreplaceable \: +\code{@Modifiable Set} and \code{@GrowShrink Set} mean the same thing. For consistency, the Modifiability Checker treats all \code{Set}s as not replaceable. \begin{itemize} @@ -162,39 +165,56 @@ \item If a \code{Set} is annotated as \code{@ShrinkReplace}, the Modifiability Checker changes it to \code{@Shrinkable}. \item If a \code{Set} is annotated as \code{@UnknownModifiability}, the - checker still reats it as \code{@UnknownModifiability} (no change). + checker still treats it as \code{@UnknownModifiability} (no change). \end{itemize} -Another example is \code{Iterator}. \code{Iterator} only has \code{remove} as an optional method, and growable and replaceable operations do not exist. To maintain consistency, the Modifiability Checker treats all iterators as not replaceable and not growable. Note that \code{ListIterator} is treated normally because it has \code{add} and \code{replace} operations. +Another example is \code{Iterator}. \code{Iterator} has the optional +\code{remove} method, but no growth or replacement operations. +For consistency, the Modifiability Checker treats all iterators as not +replaceable and not growable. Note that \code{ListIterator} is treated +normally because it has optional \code{add} and \code{set} operations. -Another example is \code{Map.Entry}. \code{Map.Entry} only has -\code{setValue} as an optional method. -To maintain consistency, the Modifiability Checker treats all \code{Map.Entry} objects as not growable and not shrinkable. +Another example is \code{Map.Entry}. \code{Map.Entry} has the optional +\code{setValue} method. +The Modifiability Checker treats all \code{Map.Entry} objects as not growable and not shrinkable. \subsectionAndLabel{Polymorphic Types}{modifiability-polymorphic} -Polymorphic annotations serve as method contracts ensuring that the output type retains specific capability bits from the input type (or receiver). +A polymorphic annotations specifies that a method's return type has the +same modifiability as one or more formal parameter types (possibly +including the receiver). \begin{description} -\item[\code{@PolyModifiable}] - Full Preservation. Output capabilities (G, S, R) exactly match the Input. Used by strict wrappers like \code{Collections.synchronizedList}. -\item[\code{@PolyShrink}] - Shrink Preservation. Output retains the \textbf{S} bit of the Input. G and R are fixed or determined separately. +\item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] + Full Preservation. Growability, shrinkability, and replacability of the + return type match those of whichever formal parameter is annotated as + \<@PolyModifiable>. + Used by strict wrappers like \code{Collections.synchronizedList}. +\item[\refqualclass{checker/modifiability/qual}{PolyShrink}] + Shrink Preservation. The return type has the same shrinkability as the + annotated formal parameter. The growability and shrinkability are + determined by non-polymorphic qualifiers from + Figure~\ref{fig-modifiability-lattice}. + + % MDE: Specify what happens if *both* a polymorphic and a + % non-polymorphic qualifier are specified, such as both @PolyShrink and + % @Modifiable. Add tests to verify the behavior. \end{description} \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} -The Modifiability Checker supports annotation that specify method -behavior. These are declaration annotations, not type annotations: they -apply to the method itself rather than to some particular type. +The Modifiability Checker supports one annotation that specify method +behavior. This is a declaration annotation, not a type annotation: it +applies to the method itself rather than to some particular type. \begin{description} \item[\refqualclass{checker/modifiability/qual}{ThrowsUOE}] This is a declaration annotation (not a type qualifier) that is applied to method and constructor declarations. It indicates that the method always throws -\code{UnsupportedOperationException}. The Modifiability Checker issues an -error for any call to a method annotated with \code{@ThrowsUOE}. +\code{UnsupportedOperationException}. (Therefore, any code that call it is +erroneous.) The Modifiability Checker issues an error at every call to a +method annotated with \code{@ThrowsUOE}. \end{description} @@ -205,6 +225,14 @@ \sectionAndLabel{What the Modifiability Checker checks}{modifiability-rules} +% MDE: I don't think that the notion of "capability" is necessary or +% useful. It is just extra jargon that users need to understand. Likewise +% for naming the bits as G, S, and R. + +% MDE: Much of this section duplicates information that is present +% elsewhere. Eliminate the duplication and put related information +% together. + The Modifiability Checker enforces that mutating methods are only called on collections that have the required modifiability capability. @@ -240,47 +268,47 @@ class Demo { - void basicUsage() { - @Modifiable List mod = new ArrayList<>(); - mod.add("a"); // OK: @Modifiable implies G=1, S=1, R=1 - mod.remove("a"); // OK - mod.set(0, "b"); // OK - - @Unmodifiable List unmod = List.of("a", "b"); - unmod.add("c"); // Error: receiver requires @Growable, found @Unmodifiable - unmod.get(0); // OK: read-only access matches any capability - } - - void fineGrainedPermissions(@Growable List g, - @Shrinkable List s, - @Replaceable List r) { - // Growable list allows adding elements (G=1) - g.add("a"); // OK - g.remove("a"); // Error: remove requires @Shrinkable - g.set(0, "b"); // Error: set requires @Replaceable - - // Shrinkable list allows removing elements (S=1) - s.remove("a"); // OK - s.add("a"); // Error: add requires @Growable - - // Replaceable list allows updating elements (R=1) - r.set(0, "b"); // OK - r.add("b"); // Error: add requires @Growable - } - - void combinedPermissions(@GrowReplace Map map) { - // Map.put requires both Grow and Replace capabilities (G=1, R=1) - map.put("key", "value"); // OK: receiver is @GrowReplace - - // Map.remove requires Shrink capability (S=1) - map.remove("key"); // Error: receiver is @GrowReplace, - // but remove requires @Shrinkable - } - - // Polymorphism preserves capabilities - @PolyModifiable List process(@PolyModifiable List list) { - return list.subList(0, 1); - } + void basicUsage() { + @Modifiable List mod = new ArrayList<>(); + mod.add("a"); // OK: @Modifiable implies G=1, S=1, R=1 + mod.remove("a"); // OK + mod.set(0, "b"); // OK + + @Unmodifiable List unmod = List.of("a", "b"); + unmod.add("c"); // Error: receiver requires @Growable, found @Unmodifiable + unmod.get(0); // OK: read-only access matches any capability + } + + void fineGrainedPermissions(@Growable List g, + @Shrinkable List s, + @Replaceable List r) { + // Growable list allows adding elements (G=1) + g.add("a"); // OK + g.remove("a"); // Error: remove requires @Shrinkable + g.set(0, "b"); // Error: set requires @Replaceable + + // Shrinkable list allows removing elements (S=1) + s.remove("a"); // OK + s.add("a"); // Error: add requires @Growable + + // Replaceable list allows updating elements (R=1) + r.set(0, "b"); // OK + r.add("b"); // Error: add requires @Growable + } + + void combinedPermissions(@GrowReplace Map map) { + // Map.put requires both Grow and Replace capabilities (G=1, R=1) + map.put("key", "value"); // OK: receiver is @GrowReplace + + // Map.remove requires Shrink capability (S=1) + map.remove("key"); // Error: receiver is @GrowReplace, + // but remove requires @Shrinkable + } + + // Polymorphism preserves capabilities + @PolyModifiable List process(@PolyModifiable List list) { + return list.subList(0, 1); + } } \end{Verbatim} @@ -289,4 +317,5 @@ % LocalWords: toArray addAll removeAll retainAll replaceAll setValue % LocalWords: UnknownModifiability Growable GrowShrink GrowReplace % LocalWords: ShrinkReplace growable ListIterator PolyModifiable addFirst -% LocalWords: synchronizedList PolyShrink ThrowsUOE addLast +% LocalWords: synchronizedList PolyShrink ThrowsUOE addLast unreplaceable +% LocalWords: Growability shrinkability replacability growability From 03eb5dda08ae48a82f47c6846f2cd928c2fe2d64 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 16 Feb 2026 19:14:37 -0800 Subject: [PATCH 040/308] fixing Mike's comments, TODO: add test for PolyShrink. --- .../ModifiabilityAnnotatedTypeFactory.java | 58 +++++++------ .../modifiability/ModifiabilityVisitor.java | 5 ++ docs/manual/modifiability-checker.tex | 84 ++++++++----------- 3 files changed, 69 insertions(+), 78 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index bb4088f4424f..b13687f6b558 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -35,20 +35,20 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory /** The {@code Collection.iterator()} method. */ private final ExecutableElement iteratorMethodElement; - /** The {@code java.util.Set} type. */ - private final TypeMirror setType; + /** The erased {@code java.util.Set} type. */ + private final TypeMirror setErasure; - /** The {@code java.util.Queue} type. */ - private final TypeMirror queueType; + /** The erased {@code java.util.Queue} type. */ + private final TypeMirror queueErasure; - /** The {@code java.util.LinkedList} type. */ - private final TypeMirror linkedListType; + /** The erased {@code java.util.LinkedList} type. */ + private final TypeMirror linkedListErasure; - /** The {@code java.util.Map.Entry} type. */ - private final TypeMirror mapEntryType; + /** The erased {@code java.util.Map.Entry} type. */ + private final TypeMirror mapEntryErasure; - /** The {@code java.util.Iterator} type. */ - private final TypeMirror iteratorType; + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; /** The {@code @}{@link UnknknownModifiability} qualifier. */ private final AnnotationMirror UNKNOWN_MODIFIABILITY; @@ -85,11 +85,15 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { this.iteratorMethodElement = TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); // Cache types - this.setType = getElementUtils().getTypeElement("java.util.Set").asType(); - this.queueType = getElementUtils().getTypeElement("java.util.Queue").asType(); - this.linkedListType = getElementUtils().getTypeElement("java.util.LinkedList").asType(); - this.mapEntryType = getElementUtils().getTypeElement("java.util.Map.Entry").asType(); - this.iteratorType = getElementUtils().getTypeElement("java.util.Iterator").asType(); + Types types = getProcessingEnv().getTypeUtils(); + this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); + this.queueErasure = types.erasure(getElementUtils().getTypeElement("java.util.Queue").asType()); + this.linkedListErasure = + types.erasure(getElementUtils().getTypeElement("java.util.LinkedList").asType()); + this.mapEntryErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); + this.iteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Cache annotation mirrors for performance. this.UNKNOWN_MODIFIABILITY = @@ -128,10 +132,9 @@ protected Set> createSupportedTypeQualifiers() { *

If a collection is {@code @Unmodifiable}, its iterator should also be treated as * {@code @Unmodifiable} (meaning {@code remove()} cannot be called). * - *

MDE: The below looks wrong. UnknownModifiability does not permit any mutations. - * - *

If a collection is {@code @Modifiable}, its iterator is {@code @UnknownModifiability} (the - * default behavior), which still permits calling {@code remove()}. + *

If a collection is {@code @Modifiable}, its iterator can be either Modifiable or + * Unmodifiable, depending on the iterator's implementation. To be conservative, we treat it as + * {@code @UnknownModifiability}, which is a supertype of both. */ @Override public ParameterizedExecutableType methodFromUse( @@ -143,7 +146,6 @@ public ParameterizedExecutableType methodFromUse( ParameterizedExecutableType mType = super.methodFromUse(tree, methodElt, receiverType, inferTypeArgs); - // MDE: Explain why using a @Poly qualifier does not work. // Special handling for the iterator() method. // We want the modifiability of the Iterator to match the modifiability of the // Collection. @@ -180,8 +182,7 @@ public ModifiabilityTypeAnnotator(ModifiabilityAnnotatedTypeFactory factory) { public Void visitDeclared(AnnotatedDeclaredType type, Void p) { super.visitDeclared(type, p); - // MDE: Does this `if` statement handle explicit bottom? The comment says it does. - // Skip if polymorphic or explicit bottom. + // Skip if polymorphic. if (type.hasPrimaryAnnotation(POLY_MODIFIABLE)) { return null; } @@ -190,19 +191,16 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { Types types = getProcessingEnv().getTypeUtils(); TypeMirror erasure = types.erasure(underlyingType); - // MDE: Rather than inefficiently and verbosely calling (e.g.) `types.erasure(setType)` - // every time that `visitDeclared` is called, that value should be stored as a field of the - // class. - if (types.isSubtype(erasure, types.erasure(setType)) - || (types.isSubtype(erasure, types.erasure(queueType)) - && !types.isSubtype(erasure, types.erasure(linkedListType)))) { + if (types.isSubtype(erasure, setErasure) + || (types.isSubtype(erasure, queueErasure) + && !types.isSubtype(erasure, linkedListErasure))) { // Set or Queue (but not LinkedList): Drop R bit removeReplaceable(type); - } else if (types.isSubtype(erasure, types.erasure(mapEntryType))) { + } else if (types.isSubtype(erasure, mapEntryErasure)) { // Map.Entry: Drop G and S bits removeGrowable(type); removeShrinkable(type); - } else if (types.isSubtype(erasure, types.erasure(iteratorType))) { + } else if (types.isSubtype(erasure, iteratorErasure)) { // Iterator: Drop G and R bits removeGrowable(type); removeReplaceable(type); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 284c3e2a2632..9e1bd3c2566f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -34,6 +34,11 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { // exist.) // Suppress the framework's "constructor result must be TOP" check. // For Modifiability, constructors may legitimately produce @Modifiable. + // By default, the BaseTypeChecker requires constructors to return the top type + // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) + // produce a @Modifiable object, which is a subtype of @UnknownModifiability. + // We want to allow this so that we can use these objects for mutation without + // explicit casting or annotations. @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index d823f17b8fd2..3d7a75dabd84 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -1,6 +1,6 @@ \chapterAndLabel{Modifiability Checker}{modifiability-checker} -In Java, an \emp{optional} method is one that a class may or may not +In Java, an \emph{optional} method is one that a class may or may not support. If the subclass does not support the method, then calling the method throws \. Thus, whenever a client calls an optional method, there is a possibility of an exception. The @@ -44,12 +44,12 @@ \textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements - (\, \, \ldots). + (\, \, \, \, \, \, \ldots). \item \textbf{Shrink}: The collection can shrink by removing - existing elements (\, \, \, \, \ldots). + existing elements (\, \, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing - elements with new values at specific locations/keys (\, \, - \, \ldots). + elements with new values at specific locations/keys (\, \, \, + \, \, \, \ldots). \end{itemize} To run the Modifiability Checker: @@ -196,9 +196,37 @@ determined by non-polymorphic qualifiers from Figure~\ref{fig-modifiability-lattice}. - % MDE: Specify what happens if *both* a polymorphic and a - % non-polymorphic qualifier are specified, such as both @PolyShrink and - % @Modifiable. Add tests to verify the behavior. + If \code{@PolyShrink} is the only modifiability qualifier present on a return +type, the capabilities for Grow and Replace default to not guaranteed. The Shrink capability is dynamically determined by the +argument. + +For example, consider the method: +\begin{Verbatim} + @PolyShrink Set keySet(@PolyShrink Map self) +\end{Verbatim} + +The resulting type of the returned set depends on the input map as follows: +\begin{itemize} + \item If the map is \code{@Modifiable}, \code{@ShrinkReplace}, \code{@GrowShrink}, or \code{@Shrinkable} the returned set is + \code{@Shrinkable}. + \item If the map is \code{@Growable}, \code{@GrowReplace}, \code{@Replaceable} or \code{@Unmodifiable}, the returned set is + \code{@Unmodifiable}. +\end{itemize} + +If both a polymorphic qualifier and a non-polymorphic qualifier are specified on +a return type, the capabilities are additive. The non-polymorphic +qualifier defines the base capabilities (the ``floor'') guaranteed for the +return type, while the polymorphic qualifier preserves the specific shrink +capability from the argument. + +For example, a method annotated with \code{@PolyShrink @Growable} returns +a \code{@GrowShrink} collection if the argument is \code{@Shrinkable}, +but returns a \code{@Growable} collection if the argument is +\code{@Unmodifiable}. If a return type is annotated with both +\code{@PolyShrink} and \code{@Modifiable}, the \code{@PolyShrink} +annotation is redundant because \code{@Modifiable} already guarantees +shrinkability. + \end{description} \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} @@ -218,47 +246,7 @@ \end{description} -% TODO: -% Each annotation’s Javadoc should include a precise description and an example -% use, and should link back to this chapter using -% \code{@checker\_framework.manual \#modifiability-checker Modifiability Checker}. - -\sectionAndLabel{What the Modifiability Checker checks}{modifiability-rules} -% MDE: I don't think that the notion of "capability" is necessary or -% useful. It is just extra jargon that users need to understand. Likewise -% for naming the bits as G, S, and R. - -% MDE: Much of this section duplicates information that is present -% elsewhere. Eliminate the duplication and put related information -% together. - -The Modifiability Checker enforces that mutating methods are only called on -collections that have the required modifiability capability. - -A \emph{mutating method} is any method that may change the size or contents of a -collection. -When annotating a method signature, the \textbf{receiver} (\code{this}) must be annotated with the minimum capabilities (G, S, R bits) required to perform the operation. - -\begin{itemize} - \item \textbf{Grow (G)}: If the method increases the size of the collection, the receiver requires \code{@Growable} (G=1). - Examples: \code{add}, \code{addAll}, \code{offer}, \code{addFirst}, \code{addLast}, \code{ListIterator.add}. - - \item \textbf{Shrink (S)}: If the method decreases the size (or removes elements), the receiver requires \code{@Shrinkable} (S=1). - Examples: \code{remove}, \code{removeAll}, \code{retainAll}, \code{clear}, \code{poll}, \code{pop}, \code{Iterator.remove}. - - \item \textbf{Replace (R)}: If the method updates an existing element \emph{at a specific location/key}, the receiver requires \code{@Replaceable} (R=1). - Examples: \code{List.set}, \code{ListIterator.set}, \code{Map.replace}, \code{Map.replaceAll}, \code{Collections.sort}, \code{Map.Entry.setValue}. - - \item \textbf{Combined Requirements}: - \begin{itemize} - \item \code{Map.put(K, V)}: Can grow OR replace. Requirement: \code{@GrowReplace} (G=1, R=1). - \item \code{Map.compute(...)} / \code{merge(...)}: Can grow, shrink, or replace. Requirement: \code{@Modifiable} (G=1, S=1, R=1). - \end{itemize} - - \item \textbf{Read-Only Operations}: If the method does not modify the structure or elements of the collection, no specific capabilities are required. The receiver can be \code{@UnknownModifiability}. - Examples: \code{get}, \code{contains}, \code{size}, \code{isEmpty}. -\end{itemize} \sectionAndLabel{Examples}{modifiability-examples} From 9886b24640f1fa28edd5d16d63e261bfa65a9bc4 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 17 Feb 2026 16:32:36 -0800 Subject: [PATCH 041/308] added stub file for ical4j --- .../checker/modifiability/ModifiabilityChecker.java | 2 ++ .../checkerframework/checker/modifiability/ical4j.astub | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 83a001b1ef38..7cdeeda1f86f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -6,6 +6,7 @@ import java.util.Map; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -25,6 +26,7 @@ Map.Entry.class, Collections.class }) +@StubFiles({"ical4j.astub"}) @SuppressWarningsPrefix({"modifiable", "unmodifiable"}) public class ModifiabilityChecker extends BaseTypeChecker { /** Creates a Modifiability checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub new file mode 100644 index 000000000000..8bac550ff5b4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.modifiability.qual.*; + +package net.fortuna.ical4j.model; + +@Modifiable class ComponentList {} + +@Modifiable class PropertyList {} \ No newline at end of file From 18ffb4fee402a16172e3da7ccfc0e83d8b44342e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 19 Feb 2026 13:25:22 -0800 Subject: [PATCH 042/308] tyring out polyShrink, not working. --- .../ModifiabilityAnnotatedTypeFactory.java | 59 +++++++++++++++++- .../tests/modifiability/PolyShrinkTest.java | 61 +++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 checker/tests/modifiability/PolyShrinkTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index b13687f6b558..d3bc573be703 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; @@ -14,6 +15,9 @@ import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolyShrink; +import org.checkerframework.checker.modifiability.qual.PolyShrinkGrow; +import org.checkerframework.checker.modifiability.qual.PolyShrinkReplace; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.ShrinkReplace; import org.checkerframework.checker.modifiability.qual.Shrinkable; @@ -21,12 +25,17 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.poly.DefaultQualifierPolymorphism; +import org.checkerframework.framework.type.poly.QualifierPolymorphism; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorMap; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; /** The type factory for the Modifiability Checker. */ @@ -77,6 +86,9 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory /** The {@code @}{@link PolyModifiable} qualifier. */ private final AnnotationMirror POLY_MODIFIABLE; + /** The {@code @}{@link PolyShrink} qualifier. */ + private final AnnotationMirror POLY_SHRINK; + @SuppressWarnings("this-escape") public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -106,6 +118,7 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.POLY_MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyModifiable.class); + this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_MODIFIABILITY); postInit(); @@ -183,7 +196,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { super.visitDeclared(type, p); // Skip if polymorphic. - if (type.hasPrimaryAnnotation(POLY_MODIFIABLE)) { + if (type.hasPrimaryAnnotation(POLY_MODIFIABLE) || type.hasPrimaryAnnotation(POLY_SHRINK)) { return null; } @@ -250,4 +263,48 @@ private void removeReplaceable(AnnotatedTypeMirror type) { type.replaceAnnotation(GROW_SHRINK); // 110 } } + + @Override + protected QualifierPolymorphism createQualifierPolymorphism() { + return new ModifiabilityQualifierPolymorphism(getProcessingEnv(), this); + } + + private class ModifiabilityQualifierPolymorphism extends DefaultQualifierPolymorphism { + public ModifiabilityQualifierPolymorphism( + ProcessingEnvironment env, AnnotatedTypeFactory factory) { + super(env, factory); + this.polyQuals.put(POLY_SHRINK, UNKNOWN_MODIFIABILITY); + this.polyQuals.put(AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkGrow.class), UNKNOWN_MODIFIABILITY); + this.polyQuals.put(AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkReplace.class), UNKNOWN_MODIFIABILITY); + } + + @Override + public void replace( + AnnotatedTypeMirror type, AnnotationMirrorMap replacements) { + if (replacements == null || replacements.isEmpty()) { + super.replace(type, replacements); + return; + } + AnnotationMirrorMap newMap = new AnnotationMirrorMap<>(replacements); + + // System.err.println("DEBUG: ModifiabilityQualifierPolymorphism.replace called. replacements=" + replacements); + + for (java.util.Map.Entry entry : replacements.entrySet()) { + AnnotationMirror poly = entry.getKey(); + AnnotationMirror qual = entry.getValue(); + + if (AnnotationUtils.areSame(poly, POLY_SHRINK)) { + // System.err.println("DEBUG: Processing PolyShrink. qual=" + qual); + if (getQualifierHierarchy().isSubtypeQualifiersOnly(qual, SHRINKABLE)) { + // System.err.println("DEBUG: qual is subtype of SHRINKABLE. replacing with SHRINKABLE."); + newMap.put(poly, SHRINKABLE); + } else { + // System.err.println("DEBUG: qual is NOT subtype of SHRINKABLE. replacing with UNKNOWN."); + newMap.put(poly, UNKNOWN_MODIFIABILITY); + } + } + } + super.replace(type, newMap); + } + } } diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java new file mode 100644 index 000000000000..e58a88c130cc --- /dev/null +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -0,0 +1,61 @@ +// import java.util.List; +// import java.util.Set; +// import java.util.Map; +// import org.checkerframework.checker.modifiability.qual.*; + +// public class PolyShrinkTest { + +// // @PolyShrink behaves like Map.keySet() +// // It preserves Shrink capability from argument. +// // Grow and Replace should be defaulted to Unknown (absent). +// @PolyShrink Set keySet(@PolyShrink Map map) { +// return null; +// } + +// void testPolyShrink( +// @Modifiable Map mod, +// @GrowShrink Map gs, +// @Growable Map g, +// @Shrinkable Map s, +// @Unmodifiable Map unmod +// ) { +// // Input: Modifiable (G, S, R) +// // Output: Shrinkable (S). +// // G and R stripped. +// @Shrinkable Set s1 = keySet(mod); +// // :: error: (assignment) +// @Growable Set g1 = keySet(mod); // No G +// // :: error: (assignment) +// @Replaceable Set r1 = keySet(mod); // No R +// // :: error: (assignment) +// @Modifiable Set m1 = keySet(mod); // No G/R + +// // Input: GrowShrink (G, S) +// // Output: Shrinkable (S). +// // G stripped. +// @Shrinkable Set s2 = keySet(gs); +// // :: error: (assignment) +// @Growable Set g2 = keySet(gs); // No G + +// // Input: Growable (G) +// // Output: UnknownModifiability (None). +// // G stripped. No S to preserve. +// @UnknownModifiability Set u3 = keySet(g); +// // :: error: (assignment) +// @Shrinkable Set s3 = keySet(g); // No S +// // :: error: (assignment) +// @Growable Set g3 = keySet(g); // No G + +// // Input: Shrinkable (S) +// // Output: Shrinkable (S). +// @Shrinkable Set s4 = keySet(s); +// // :: error: (assignment) +// @Growable Set g4 = keySet(s); + +// // Input: Unmodifiable (None) +// // Output: UnknownModifiability. +// @UnknownModifiability Set u5 = keySet(unmod); +// // :: error: (assignment) +// @Shrinkable Set s5 = keySet(unmod); +// } +// } From eb0f1272874beff92d0795f7d1e7ad884e6e60c1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 19 Feb 2026 13:25:33 -0800 Subject: [PATCH 043/308] edit format --- .../ModifiabilityAnnotatedTypeFactory.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index d3bc573be703..da3c3832f9ac 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -274,8 +274,12 @@ public ModifiabilityQualifierPolymorphism( ProcessingEnvironment env, AnnotatedTypeFactory factory) { super(env, factory); this.polyQuals.put(POLY_SHRINK, UNKNOWN_MODIFIABILITY); - this.polyQuals.put(AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkGrow.class), UNKNOWN_MODIFIABILITY); - this.polyQuals.put(AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkReplace.class), UNKNOWN_MODIFIABILITY); + this.polyQuals.put( + AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkGrow.class), + UNKNOWN_MODIFIABILITY); + this.polyQuals.put( + AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkReplace.class), + UNKNOWN_MODIFIABILITY); } @Override @@ -287,19 +291,23 @@ public void replace( } AnnotationMirrorMap newMap = new AnnotationMirrorMap<>(replacements); - // System.err.println("DEBUG: ModifiabilityQualifierPolymorphism.replace called. replacements=" + replacements); + // System.err.println("DEBUG: ModifiabilityQualifierPolymorphism.replace called. + // replacements=" + replacements); - for (java.util.Map.Entry entry : replacements.entrySet()) { + for (java.util.Map.Entry entry : + replacements.entrySet()) { AnnotationMirror poly = entry.getKey(); AnnotationMirror qual = entry.getValue(); if (AnnotationUtils.areSame(poly, POLY_SHRINK)) { // System.err.println("DEBUG: Processing PolyShrink. qual=" + qual); if (getQualifierHierarchy().isSubtypeQualifiersOnly(qual, SHRINKABLE)) { - // System.err.println("DEBUG: qual is subtype of SHRINKABLE. replacing with SHRINKABLE."); + // System.err.println("DEBUG: qual is subtype of SHRINKABLE. replacing with + // SHRINKABLE."); newMap.put(poly, SHRINKABLE); } else { - // System.err.println("DEBUG: qual is NOT subtype of SHRINKABLE. replacing with UNKNOWN."); + // System.err.println("DEBUG: qual is NOT subtype of SHRINKABLE. replacing with + // UNKNOWN."); newMap.put(poly, UNKNOWN_MODIFIABILITY); } } From b1ed8f5a0f4fde96a4cb0759099d41d64ee38630 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 19 Feb 2026 15:55:15 -0800 Subject: [PATCH 044/308] Refactor Modifiability Checker to use 3 2-level hierarchy. edited the Hierarchy test edited ModifiabilityAnnotatedTypeFactory. Right now there is still some issue in creating alias. --- .../checker/modifiability/qual/Growable.java | 2 +- .../modifiability/qual/Modifiable.java | 10 +- .../checker/modifiability/qual/PolyGrow.java | 23 ++ .../modifiability/qual/PolyModifiable.java | 11 +- .../modifiability/qual/PolyReplace.java | 23 ++ .../modifiability/qual/PolyShrink.java | 6 +- .../modifiability/qual/Replaceable.java | 2 +- .../modifiability/qual/Shrinkable.java | 2 +- .../{GrowReplace.java => UnknownGrow.java} | 12 +- .../qual/UnknownModifiability.java | 15 +- ...ShrinkReplace.java => UnknownReplace.java} | 13 +- .../{GrowShrink.java => UnknownShrink.java} | 13 +- .../modifiability/qual/Unmodifiable.java | 8 +- .../ModifiabilityAnnotatedTypeFactory.java | 337 +++++++--------- .../tests/modifiability/HierarchyTest.java | 279 +++++++------ docs/manual/modifiability-checker.tex | 378 +++++++++++------- 16 files changed, 614 insertions(+), 520 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{GrowReplace.java => UnknownGrow.java} (54%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{ShrinkReplace.java => UnknownReplace.java} (52%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{GrowShrink.java => UnknownShrink.java} (52%) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java index 3c233812a5f1..5c2b1b7315c1 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownModifiability.class) +@SubtypeOf(UnknownGrow.class) public @interface Growable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 2bff5ec0635f..97d492135de4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -5,18 +5,18 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** - * Calling mutating operations such as {@code add}, {@code remove}, {@code clear}, etc. on this - * collection will not result in throwing {@link UnsupportedOperationException}. + * Convenience alias meaning {@code @Growable @Shrinkable @Replaceable}. Calling grow, shrink, and + * replace operations such as {@code add}, {@code remove}, {@code set}, etc. on this collection will + * not result in throwing {@link UnsupportedOperationException}. * - *

MDE: Mention the non-guarantee, as is done for other qualifiers. + *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to + * {@code @Growable @Shrinkable @Replaceable} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({GrowShrink.class, GrowReplace.class, ShrinkReplace.class}) public @interface Modifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java new file mode 100644 index 000000000000..3a4d5b43fbf8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.modifiability.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 Grow hierarchy. + * + *

When used on a method, the Grow capability of the return type matches the Grow capability of + * the argument or receiver annotated with {@code @PolyGrow}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(UnknownGrow.class) +public @interface PolyGrow {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 0acaef56339e..73bc67261eb3 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -2,16 +2,18 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.PolymorphicQualifier; /** - * A polymorphic qualifier for modifiability. + * Convenience alias meaning {@code @PolyGrow @PolyShrink @PolyReplace}. A polymorphic qualifier for + * all three modifiability hierarchies. + * + *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to + * {@code @PolyGrow @PolyShrink @PolyReplace} on each annotated type. * *

Use on methods that preserve or transfer modifiability — for example, {@code * List.subList()}, {@code iterator()}, or {@code stream()}. * - *

The same qualifier that appears on the receiver will also appear on the return type (and - * possibly parameters). For example: + *

For example: * *


  * class Example {
@@ -25,6 +27,5 @@
  * @checker_framework.manual #modifiability-checker Modifiability Checker
  * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism
  */
-@PolymorphicQualifier(UnknownModifiability.class)
 @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
 public @interface PolyModifiable {}
diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java
new file mode 100644
index 000000000000..eb6e7ac5af7d
--- /dev/null
+++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java
@@ -0,0 +1,23 @@
+package org.checkerframework.checker.modifiability.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 Replace hierarchy.
+ *
+ * 

When used on a method, the Replace capability of the return type matches the Replace + * capability of the argument or receiver annotated with {@code @PolyReplace}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(UnknownReplace.class) +public @interface PolyReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java index c73d29d0590b..b491d8626f99 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java @@ -8,17 +8,15 @@ import org.checkerframework.framework.qual.PolymorphicQualifier; /** - * A polymorphic qualifier for modifiability that preserves the shrink capability. + * A polymorphic qualifier for the Shrink hierarchy that preserves the shrink capability. * *

Use on methods that preserve shrinkability — for example, {@code Map.keySet()}. * * @checker_framework.manual #modifiability-checker Modifiability Checker - *

TODO: need to implement - * @checker_framework.manual #modifiability-checker Modifiability Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(UnknownModifiability.class) +@PolymorphicQualifier(UnknownShrink.class) public @interface PolyShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java index 20b7b79b1f56..bc6199b61f62 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownModifiability.class) +@SubtypeOf(UnknownReplace.class) public @interface Replaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java index 45224b72d796..cabe5a275869 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownModifiability.class) +@SubtypeOf(UnknownShrink.class) public @interface Shrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java similarity index 54% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowReplace.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java index b3b55aa56a64..30f3c460deec 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java @@ -5,18 +5,20 @@ 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; /** - * Calling grow and replace operations on this collection will not result in throwing {@link - * UnsupportedOperationException}. + * The top qualifier in the Grow hierarchy. Represents an unknown or absent grow capability. The + * checker cannot determine whether the collection supports grow operations such as {@code add}. * - *

No guarantees are made about shrink operations. + *

This is the default qualifier for unannotated types in the Grow hierarchy. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({Growable.class, Replaceable.class}) -public @interface GrowReplace {} +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface UnknownGrow {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java index e84e6cdb0cc0..f653f09d2d03 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java @@ -5,22 +5,19 @@ 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; /** - * Represents an unknown or arbitrary modifiability capability. The checker cannot determine whether - * the collection is growable, shrinkable, or replaceable. The Modifiability Checker issues an error - * if code calls any modification operation on a collection with this type. + * Convenience alias meaning {@code @UnknownGrow @UnknownShrink @UnknownReplace}. Represents an + * unknown or arbitrary modifiability capability; the checker cannot determine whether the + * collection is growable, shrinkable, or replaceable. * - *

This is the default qualifier for unannotated types. This is the top qualifier in the - * hierarchy. + *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to + * {@code @UnknownGrow @UnknownShrink @UnknownReplace} on each annotated type. It is equivalent to + * {@link Unmodifiable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({}) -@DefaultQualifierInHierarchy public @interface UnknownModifiability {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java similarity index 52% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java index cd263b97f3ad..a793d64f521f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ShrinkReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java @@ -5,18 +5,21 @@ 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; /** - * Calling shrink and replace operations on this collection will not result in throwing {@link - * UnsupportedOperationException}. + * The top qualifier in the Replace hierarchy. Represents an unknown or absent replace capability. + * The checker cannot determine whether the collection supports replace operations such as {@code + * set} or {@code replaceAll}. * - *

No guarantees are made about grow operations. + *

This is the default qualifier for unannotated types in the Replace hierarchy. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({Shrinkable.class, Replaceable.class}) -public @interface ShrinkReplace {} +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface UnknownReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java similarity index 52% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java index 564b19a35596..7f87345b521c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/GrowShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java @@ -5,18 +5,21 @@ 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; /** - * Calling grow and shrink operations on this collection will not result in throwing {@link - * UnsupportedOperationException}. + * The top qualifier in the Shrink hierarchy. Represents an unknown or absent shrink capability. The + * checker cannot determine whether the collection supports shrink operations such as {@code remove} + * or {@code clear}. * - *

No guarantees are made about replace operations. + *

This is the default qualifier for unannotated types in the Shrink hierarchy. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({Growable.class, Shrinkable.class}) -public @interface GrowShrink {} +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface UnknownShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 0152cdc84bdf..f5252ff71572 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -7,8 +7,12 @@ import java.lang.annotation.Target; /** - * The collection is known to be unmodifiable. Calling a mutating (growing, shrinking, or replacing) - * method such as {@code add} or {@code remove} always throws {@link UnsupportedOperationException}. + * Convenience alias meaning {@code @UnknownGrow @UnknownShrink @UnknownReplace}. Calling any + * mutating operation on this collection (growing, shrinking, or replacing) will throw {@link + * UnsupportedOperationException}. + * + *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to + * {@code @UnknownGrow @UnknownShrink @UnknownReplace} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index da3c3832f9ac..6c7ba559ce84 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -1,49 +1,46 @@ package org.checkerframework.checker.modifiability; -import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; -import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Element; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -import org.checkerframework.checker.modifiability.qual.GrowReplace; -import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyGrow; import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolyReplace; import org.checkerframework.checker.modifiability.qual.PolyShrink; -import org.checkerframework.checker.modifiability.qual.PolyShrinkGrow; -import org.checkerframework.checker.modifiability.qual.PolyShrinkReplace; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.ShrinkReplace; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnknownGrow; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.UnknownReplace; +import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.framework.type.poly.DefaultQualifierPolymorphism; -import org.checkerframework.framework.type.poly.QualifierPolymorphism; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationMirrorMap; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; /** The type factory for the Modifiability Checker. */ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - /** The {@code Collection.iterator()} method. */ - private final ExecutableElement iteratorMethodElement; - /** The erased {@code java.util.Set} type. */ private final TypeMirror setErasure; @@ -59,44 +56,53 @@ public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory /** The erased {@code java.util.Iterator} type. */ private final TypeMirror iteratorErasure; - /** The {@code @}{@link UnknknownModifiability} qualifier. */ - private final AnnotationMirror UNKNOWN_MODIFIABILITY; - - /** The {@code @}{@link Modifiable} qualifier. */ - private final AnnotationMirror MODIFIABLE; + // ── Hierarchy qualifiers (9 total: 3 tops + 3 bottoms + 3 poly) ────────── - /** The {@code @}{@link GrowShrink} qualifier. */ - private final AnnotationMirror GROW_SHRINK; + /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ + private AnnotationMirror UNKNOWN_GROW; - /** The {@code @}{@link GrowReplace} qualifier. */ - private final AnnotationMirror GROW_REPLACE; + /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ + private AnnotationMirror UNKNOWN_SHRINK; - /** The {@code @}{@link ShrinkReplace} qualifier. */ - private final AnnotationMirror SHRINK_REPLACE; + /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ + private AnnotationMirror UNKNOWN_REPLACE; - /** The {@code @}{@link ShrinkReplace} qualifier. */ - private final AnnotationMirror GROWABLE; + /** The {@code @}{@link Growable} qualifier (bottom of Grow hierarchy). */ + private AnnotationMirror GROWABLE; - /** The {@code @}{@link Growable} qualifier. */ - private final AnnotationMirror SHRINKABLE; + /** The {@code @}{@link Shrinkable} qualifier (bottom of Shrink hierarchy). */ + private AnnotationMirror SHRINKABLE; - /** The {@code @}{@link Replaceable} qualifier. */ - private final AnnotationMirror REPLACEABLE; + /** The {@code @}{@link Replaceable} qualifier (bottom of Replace hierarchy). */ + private AnnotationMirror REPLACEABLE; - /** The {@code @}{@link PolyModifiable} qualifier. */ - private final AnnotationMirror POLY_MODIFIABLE; + /** The {@code @}{@link PolyGrow} qualifier. */ + private AnnotationMirror POLY_GROW; /** The {@code @}{@link PolyShrink} qualifier. */ - private final AnnotationMirror POLY_SHRINK; + private AnnotationMirror POLY_SHRINK; + + /** The {@code @}{@link PolyReplace} qualifier. */ + private AnnotationMirror POLY_REPLACE; + + // ── Alias qualifiers (not in hierarchy; expanded by the tree annotator) ── + + /** The {@code @}{@link Modifiable} alias annotation. */ + private AnnotationMirror MODIFIABLE; + + /** The {@code @}{@link Unmodifiable} alias annotation. */ + private AnnotationMirror UNMODIFIABLE; + + /** The {@code @}{@link UnknownModifiability} alias annotation. */ + private AnnotationMirror UNKNOWN_MODIFIABILITY; + + /** The {@code @}{@link PolyModifiable} alias annotation. */ + private AnnotationMirror POLY_MODIFIABLE; @SuppressWarnings("this-escape") public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - // Cache the Element for java.lang.Iterable.iterator() to allow fast comparisons - // later. - this.iteratorMethodElement = - TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv); - // Cache types + // Cache type erasures. Types types = getProcessingEnv().getTypeUtils(); this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); this.queueErasure = types.erasure(getElementUtils().getTypeElement("java.util.Queue").asType()); @@ -107,78 +113,103 @@ public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { this.iteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); - // Cache annotation mirrors for performance. - this.UNKNOWN_MODIFIABILITY = - AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); - this.MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Modifiable.class); - this.GROW_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), GrowShrink.class); - this.GROW_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), GrowReplace.class); - this.SHRINK_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), ShrinkReplace.class); + postInit(); + } + + @Override + protected void postInit() { + super.postInit(); + // Initialize annotation mirrors after the hierarchy is established. + this.UNKNOWN_GROW = AnnotationBuilder.fromClass(getElementUtils(), UnknownGrow.class); + this.UNKNOWN_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), UnknownShrink.class); + this.UNKNOWN_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), UnknownReplace.class); this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); - this.POLY_MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyModifiable.class); + this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); - - addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_MODIFIABILITY); - postInit(); + this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); + this.MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Modifiable.class); + this.UNMODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Unmodifiable.class); + this.UNKNOWN_MODIFIABILITY = + AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); + this.POLY_MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyModifiable.class); } @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - UnknownModifiability.class, - Modifiable.class, - PolyModifiable.class, - GrowReplace.class, - GrowShrink.class, + // Grow hierarchy + UnknownGrow.class, Growable.class, + PolyGrow.class, + // Shrink hierarchy + UnknownShrink.class, + Shrinkable.class, + PolyShrink.class, + // Replace hierarchy + UnknownReplace.class, Replaceable.class, - ShrinkReplace.class, - Shrinkable.class)); + PolyReplace.class)); } /** - * Refines the return type of the {@code iterator()} method based on the receiver's modifiability. - * - *

If a collection is {@code @Unmodifiable}, its iterator should also be treated as - * {@code @Unmodifiable} (meaning {@code remove()} cannot be called). - * - *

If a collection is {@code @Modifiable}, its iterator can be either Modifiable or - * Unmodifiable, depending on the iterator's implementation. To be conservative, we treat it as - * {@code @UnknownModifiability}, which is a supertype of both. + * Adds a tree annotator that expands alias annotations ({@code @Modifiable}, + * {@code @Unmodifiable}, {@code @UnknownModifiability}, {@code @PolyModifiable}) into their + * constituent qualifiers across all three hierarchies. */ @Override - public ParameterizedExecutableType methodFromUse( - ExpressionTree tree, - ExecutableElement methodElt, - AnnotatedTypeMirror receiverType, - boolean inferTypeArgs) { - - ParameterizedExecutableType mType = - super.methodFromUse(tree, methodElt, receiverType, inferTypeArgs); - - // Special handling for the iterator() method. - // We want the modifiability of the Iterator to match the modifiability of the - // Collection. - if (TreeUtils.isMethodInvocation(tree, iteratorMethodElement, processingEnv)) { - - AnnotatedTypeMirror returnType = mType.executableType.getReturnType(); - - if (receiverType.hasPrimaryAnnotation(Unmodifiable.class)) { - // Collection.iterator() on an @Unmodifiable collection returns an @Unmodifiable - // Iterator. - returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); - } else if (receiverType.hasPrimaryAnnotation(Modifiable.class)) { - // Collection.iterator() on a @Modifiable collection returns an - // @UnknownModifiability Iterator - // (defaulting to bottom in this hierarchy is treated as safe for use). - returnType.replaceAnnotation(UNKNOWN_MODIFIABILITY); - } + protected TreeAnnotator createTreeAnnotator() { + return new ListTreeAnnotator( + new ModifiabilityAliasAnnotator(this), super.createTreeAnnotator()); + } + + /** + * Expands alias annotations written in source code into their equivalent multi-hierarchy + * qualifiers: + * + *

    + *
  • {@code @Modifiable} → {@code @Growable @Shrinkable @Replaceable} + *
  • {@code @Unmodifiable} or {@code @UnknownModifiability} → + * {@code @UnknownGrow @UnknownShrink @UnknownReplace} + *
  • {@code @PolyModifiable} → {@code @PolyGrow @PolyShrink @PolyReplace} + *
+ */ + private class ModifiabilityAliasAnnotator extends TreeAnnotator { + public ModifiabilityAliasAnnotator(AnnotatedTypeFactory factory) { + super(factory); } - return mType; + /** + * Expands alias annotations found in the given list onto the given type. + * + * @param rawAnnos raw annotations from the source tree + * @param type the type to modify + */ + private void expandAliases( + List rawAnnos, AnnotatedTypeMirror type) { + for (AnnotationMirror anno : rawAnnos) { + if (AnnotationUtils.areSame(anno, MODIFIABLE)) { + type.replaceAnnotation(GROWABLE); + type.replaceAnnotation(SHRINKABLE); + type.replaceAnnotation(REPLACEABLE); + break; + } else if (AnnotationUtils.areSame(anno, UNMODIFIABLE) + || AnnotationUtils.areSame(anno, UNKNOWN_MODIFIABILITY)) { + type.replaceAnnotation(UNKNOWN_GROW); + type.replaceAnnotation(UNKNOWN_SHRINK); + type.replaceAnnotation(UNKNOWN_REPLACE); + break; + } else if (AnnotationUtils.areSame(anno, POLY_MODIFIABLE)) { + type.replaceAnnotation(POLY_GROW); + type.replaceAnnotation(POLY_SHRINK); + type.replaceAnnotation(POLY_REPLACE); + break; + } + } + return super.visitAnnotatedType(node, type); + } } @Override @@ -186,6 +217,16 @@ protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator(new ModifiabilityTypeAnnotator(this), super.createTypeAnnotator()); } + /** + * Removes capabilities that cannot be supported by structural constraints of the collection type: + * + *
    + *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to + * {@code @UnknownReplace} + *
  • Map.Entry: remove Grow and Shrink capabilities + *
  • Iterator: remove Grow and Replace capabilities + *
+ */ private class ModifiabilityTypeAnnotator extends TypeAnnotator { public ModifiabilityTypeAnnotator(ModifiabilityAnnotatedTypeFactory factory) { super(factory); @@ -195,8 +236,10 @@ public ModifiabilityTypeAnnotator(ModifiabilityAnnotatedTypeFactory factory) { public Void visitDeclared(AnnotatedDeclaredType type, Void p) { super.visitDeclared(type, p); - // Skip if polymorphic. - if (type.hasPrimaryAnnotation(POLY_MODIFIABLE) || type.hasPrimaryAnnotation(POLY_SHRINK)) { + // Skip structural refinement for polymorphic types. + if (type.hasPrimaryAnnotation(POLY_GROW) + || type.hasPrimaryAnnotation(POLY_SHRINK) + || type.hasPrimaryAnnotation(POLY_REPLACE)) { return null; } @@ -207,14 +250,14 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { if (types.isSubtype(erasure, setErasure) || (types.isSubtype(erasure, queueErasure) && !types.isSubtype(erasure, linkedListErasure))) { - // Set or Queue (but not LinkedList): Drop R bit + // Set or Queue (but not LinkedList): no replace methods → remove Replace capability. removeReplaceable(type); } else if (types.isSubtype(erasure, mapEntryErasure)) { - // Map.Entry: Drop G and S bits + // Map.Entry: no grow or shrink methods → remove both capabilities. removeGrowable(type); removeShrinkable(type); } else if (types.isSubtype(erasure, iteratorErasure)) { - // Iterator: Drop G and R bits + // Iterator: no grow or replace methods → remove both capabilities. removeGrowable(type); removeReplaceable(type); } @@ -223,96 +266,26 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { } } - // The text about "bits" comes out of the blue. It should have documentation, or it should - // cross-reference documentation. - // Helper to remove 'Grow' capability (bit 100) + /** + * Removes the Grow capability by setting the Grow hierarchy qualifier to {@code @UnknownGrow}. + */ private void removeGrowable(AnnotatedTypeMirror type) { - if (type.hasPrimaryAnnotation(GROWABLE)) { // 100 - type.replaceAnnotation(UNKNOWN_MODIFIABILITY); // 000 - } else if (type.hasPrimaryAnnotation(GROW_SHRINK)) { // 110 - type.replaceAnnotation(SHRINKABLE); // 010 - } else if (type.hasPrimaryAnnotation(GROW_REPLACE)) { // 101 - type.replaceAnnotation(REPLACEABLE); // 001 - } else if (type.hasPrimaryAnnotation(MODIFIABLE)) { // 111 - type.replaceAnnotation(SHRINK_REPLACE); // 011 - } + type.replaceAnnotation(UNKNOWN_GROW); } - // Helper to remove 'Shrink' capability (bit 010) + /** + * Removes the Shrink capability by setting the Shrink hierarchy qualifier to + * {@code @UnknownShrink}. + */ private void removeShrinkable(AnnotatedTypeMirror type) { - if (type.hasPrimaryAnnotation(SHRINKABLE)) { // 010 - type.replaceAnnotation(UNKNOWN_MODIFIABILITY); // 000 - } else if (type.hasPrimaryAnnotation(GROW_SHRINK)) { // 110 - type.replaceAnnotation(GROWABLE); // 100 - } else if (type.hasPrimaryAnnotation(SHRINK_REPLACE)) { // 011 - type.replaceAnnotation(REPLACEABLE); // 001 - } else if (type.hasPrimaryAnnotation(MODIFIABLE)) { // 111 - type.replaceAnnotation(GROW_REPLACE); // 101 - } + type.replaceAnnotation(UNKNOWN_SHRINK); } - // Helper to remove 'Replace' capability (bit 001) + /** + * Removes the Replace capability by setting the Replace hierarchy qualifier to + * {@code @UnknownReplace}. + */ private void removeReplaceable(AnnotatedTypeMirror type) { - if (type.hasPrimaryAnnotation(REPLACEABLE)) { // 001 - type.replaceAnnotation(UNKNOWN_MODIFIABILITY); // 000 - } else if (type.hasPrimaryAnnotation(GROW_REPLACE)) { // 101 - type.replaceAnnotation(GROWABLE); // 100 - } else if (type.hasPrimaryAnnotation(SHRINK_REPLACE)) { // 011 - type.replaceAnnotation(SHRINKABLE); // 010 - } else if (type.hasPrimaryAnnotation(MODIFIABLE)) { // 111 - type.replaceAnnotation(GROW_SHRINK); // 110 - } - } - - @Override - protected QualifierPolymorphism createQualifierPolymorphism() { - return new ModifiabilityQualifierPolymorphism(getProcessingEnv(), this); - } - - private class ModifiabilityQualifierPolymorphism extends DefaultQualifierPolymorphism { - public ModifiabilityQualifierPolymorphism( - ProcessingEnvironment env, AnnotatedTypeFactory factory) { - super(env, factory); - this.polyQuals.put(POLY_SHRINK, UNKNOWN_MODIFIABILITY); - this.polyQuals.put( - AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkGrow.class), - UNKNOWN_MODIFIABILITY); - this.polyQuals.put( - AnnotationBuilder.fromClass(factory.getElementUtils(), PolyShrinkReplace.class), - UNKNOWN_MODIFIABILITY); - } - - @Override - public void replace( - AnnotatedTypeMirror type, AnnotationMirrorMap replacements) { - if (replacements == null || replacements.isEmpty()) { - super.replace(type, replacements); - return; - } - AnnotationMirrorMap newMap = new AnnotationMirrorMap<>(replacements); - - // System.err.println("DEBUG: ModifiabilityQualifierPolymorphism.replace called. - // replacements=" + replacements); - - for (java.util.Map.Entry entry : - replacements.entrySet()) { - AnnotationMirror poly = entry.getKey(); - AnnotationMirror qual = entry.getValue(); - - if (AnnotationUtils.areSame(poly, POLY_SHRINK)) { - // System.err.println("DEBUG: Processing PolyShrink. qual=" + qual); - if (getQualifierHierarchy().isSubtypeQualifiersOnly(qual, SHRINKABLE)) { - // System.err.println("DEBUG: qual is subtype of SHRINKABLE. replacing with - // SHRINKABLE."); - newMap.put(poly, SHRINKABLE); - } else { - // System.err.println("DEBUG: qual is NOT subtype of SHRINKABLE. replacing with - // UNKNOWN."); - newMap.put(poly, UNKNOWN_MODIFIABILITY); - } - } - } - super.replace(type, newMap); - } + type.replaceAnnotation(UNKNOWN_REPLACE); } } diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index 0254220ad444..098a2d139fe1 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -1,177 +1,170 @@ -import org.checkerframework.checker.modifiability.qual.GrowReplace; -import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.ShrinkReplace; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnknownGrow; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.UnknownReplace; +import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +/** + * Tests the three independent 2-element lattices of the Modifiability Checker. + * + *

Grow hierarchy: UnknownGrow (top/default) ← Growable (bottom) Shrink hierarchy: UnknownShrink + * (top/default) ← Shrinkable (bottom) Replace hierarchy: UnknownReplace (top/default) ← Replaceable + * (bottom) + * + *

An annotated type carries one qualifier per hierarchy. Assignment is valid if and only if the + * RHS is a subtype in every hierarchy simultaneously. + */ class HierarchyTest { - void test( - @Modifiable Object mod, - @GrowShrink Object gs, - @GrowReplace Object gr, - @ShrinkReplace Object sr, - @Growable Object g, - @Shrinkable Object s, - @Replaceable Object r, - @UnknownModifiability Object unknown, - @Unmodifiable Object unmod) { - // ============================================================ - // Assignments to @UnknownModifiability (Top) - // ============================================================ - @UnknownModifiability Object u1 = mod; - @UnknownModifiability Object u2 = gs; - @UnknownModifiability Object u3 = gr; - @UnknownModifiability Object u4 = sr; - @UnknownModifiability Object u5 = g; - @UnknownModifiability Object u6 = s; - @UnknownModifiability Object u7 = r; - @UnknownModifiability Object u8 = unknown; - @UnknownModifiability Object u9 = unmod; - // ============================================================ - // Assignments to @Unmodifiable (Alias to Top) - // ============================================================ - @Unmodifiable Object um1 = mod; - @Unmodifiable Object um2 = gs; - @Unmodifiable Object um3 = gr; - @Unmodifiable Object um4 = sr; - @Unmodifiable Object um5 = g; - @Unmodifiable Object um6 = s; - @Unmodifiable Object um7 = r; - @Unmodifiable Object um8 = unknown; - @Unmodifiable Object um9 = unmod; + // ============================================================ + // Grow hierarchy + // ============================================================ + void testGrow(@Growable Object g, @UnknownGrow Object u) { + // Growable <: UnknownGrow (bottom to top is always OK) + @UnknownGrow Object u1 = g; + // :: error: (assignment) + @Growable Object g1 = u; // UnknownGrow !<: Growable + } - // ============================================================ - // Assignments to @Growable (G) - // ============================================================ - @Growable Object g1 = mod; - @Growable Object g2 = gs; - @Growable Object g3 = gr; - // :: error: (assignment) - @Growable Object g4 = sr; - @Growable Object g5 = g; - // :: error: (assignment) - @Growable Object g6 = s; - // :: error: (assignment) - @Growable Object g7 = r; - // :: error: (assignment) - @Growable Object g8 = unknown; + // ============================================================ + // Shrink hierarchy + // ============================================================ + void testShrink(@Shrinkable Object s, @UnknownShrink Object u) { + @UnknownShrink Object u1 = s; // :: error: (assignment) - @Growable Object g9 = unmod; + @Shrinkable Object s1 = u; + } - // ============================================================ - // Assignments to @Shrinkable (S) - // ============================================================ - @Shrinkable Object s1 = mod; - @Shrinkable Object s2 = gs; - // :: error: (assignment) - @Shrinkable Object s3 = gr; - @Shrinkable Object s4 = sr; - // :: error: (assignment) - @Shrinkable Object s5 = g; - @Shrinkable Object s6 = s; - // :: error: (assignment) - @Shrinkable Object s7 = r; + // ============================================================ + // Replace hierarchy + // ============================================================ + void testReplace(@Replaceable Object r, @UnknownReplace Object u) { + @UnknownReplace Object u1 = r; // :: error: (assignment) - @Shrinkable Object s8 = unknown; - // :: error: (assignment) - @Shrinkable Object s9 = unmod; + @Replaceable Object r1 = u; + } - // ============================================================ - // Assignments to @Replaceable (R) - // ============================================================ - @Replaceable Object r1 = mod; - // :: error: (assignment) - @Replaceable Object r2 = gs; - @Replaceable Object r3 = gr; - @Replaceable Object r4 = sr; - // :: error: (assignment) - @Replaceable Object r5 = g; - // :: error: (assignment) - @Replaceable Object r6 = s; - @Replaceable Object r7 = r; - // :: error: (assignment) - @Replaceable Object r8 = unknown; - // :: error: (assignment) - @Replaceable Object r9 = unmod; + // ============================================================ + // Multi-annotation combinations (one qualifier per hierarchy) + // ============================================================ + void testCombinations( + @Growable @Shrinkable @Replaceable Object gsr, // full capabilities + @Growable @Shrinkable Object gs, // Grow + Shrink only (Replace = UnknownReplace default) + @Growable @Replaceable Object gr, // Grow + Replace only (Shrink = UnknownShrink default) + @Shrinkable @Replaceable Object sr, // Shrink + Replace only (Grow = UnknownGrow default) + @Growable Object g, // Grow only + @Shrinkable Object s, // Shrink only + @Replaceable Object r, // Replace only + @UnknownGrow @UnknownShrink @UnknownReplace Object none) { // no capabilities - // ============================================================ - // Assignments to @GrowShrink (G, S) - // ============================================================ - @GrowShrink Object gs1 = mod; - @GrowShrink Object gs2 = gs; - // :: error: (assignment) - @GrowShrink Object gs3 = gr; - // :: error: (assignment) - @GrowShrink Object gs4 = sr; + // ── Assignments to @Growable ────────────────────────────── + @Growable Object gv1 = gsr; // G+S+R <: G + @Growable Object gv2 = gs; // G+S <: G + @Growable Object gv3 = gr; // G+R <: G // :: error: (assignment) - @GrowShrink Object gs5 = g; + @Growable Object gv4 = sr; // S+R: no G bit + @Growable Object gv5 = g; // :: error: (assignment) - @GrowShrink Object gs6 = s; + @Growable Object gv6 = s; // :: error: (assignment) - @GrowShrink Object gs7 = r; + @Growable Object gv7 = r; // :: error: (assignment) - @GrowShrink Object gs8 = unknown; + @Growable Object gv8 = none; - // ============================================================ - // Assignments to @GrowReplace (G, R) - // ============================================================ - @GrowReplace Object gr1 = mod; + // ── Assignments to @Shrinkable ──────────────────────────── + @Shrinkable Object sv1 = gsr; + @Shrinkable Object sv2 = gs; // :: error: (assignment) - @GrowReplace Object gr2 = gs; - @GrowReplace Object gr3 = gr; + @Shrinkable Object sv3 = gr; // G+R: no S bit + @Shrinkable Object sv4 = sr; // :: error: (assignment) - @GrowReplace Object gr4 = sr; + @Shrinkable Object sv5 = g; + @Shrinkable Object sv6 = s; // :: error: (assignment) - @GrowReplace Object gr5 = g; + @Shrinkable Object sv7 = r; // :: error: (assignment) - @GrowReplace Object gr6 = s; - // :: error: (assignment) - @GrowReplace Object gr7 = r; - // :: error: (assignment) - @GrowReplace Object gr8 = unknown; + @Shrinkable Object sv8 = none; - // ============================================================ - // Assignments to @ShrinkReplace (S, R) - // ============================================================ - @ShrinkReplace Object sr1 = mod; - // :: error: (assignment) - @ShrinkReplace Object sr2 = gs; + // ── Assignments to @Replaceable ─────────────────────────── + @Replaceable Object rv1 = gsr; // :: error: (assignment) - @ShrinkReplace Object sr3 = gr; - @ShrinkReplace Object sr4 = sr; + @Replaceable Object rv2 = gs; // G+S: no R bit + @Replaceable Object rv3 = gr; + @Replaceable Object rv4 = sr; // :: error: (assignment) - @ShrinkReplace Object sr5 = g; + @Replaceable Object rv5 = g; // :: error: (assignment) - @ShrinkReplace Object sr6 = s; + @Replaceable Object rv6 = s; + @Replaceable Object rv7 = r; // :: error: (assignment) - @ShrinkReplace Object sr7 = r; - // :: error: (assignment) - @ShrinkReplace Object sr8 = unknown; + @Replaceable Object rv8 = none; - // ============================================================ - // Assignments to @Modifiable (Bottom) - // ============================================================ - @Modifiable Object m1 = mod; - // :: error: (assignment) - @Modifiable Object m2 = gs; - // :: error: (assignment) - @Modifiable Object m3 = gr; - // :: error: (assignment) - @Modifiable Object m4 = sr; - // :: error: (assignment) - @Modifiable Object m5 = g; - // :: error: (assignment) - @Modifiable Object m6 = s; - // :: error: (assignment) - @Modifiable Object m7 = r; + // ── Assignments to full @UnknownGrow @UnknownShrink @UnknownReplace (top in all) ── + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv1 = gsr; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv2 = gs; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv3 = gr; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv4 = sr; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv5 = g; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv6 = s; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv7 = r; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Object tv8 = none; + } + + // ============================================================ + // Alias annotations + // ============================================================ + void testAliases( + @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object unknown) { + + // @Modifiable expands to @Growable @Shrinkable @Replaceable (full capabilities) + @Growable Object g1 = mod; + @Shrinkable Object s1 = mod; + @Replaceable Object r1 = mod; + + // @Unmodifiable / @UnknownModifiability expand to @UnknownGrow @UnknownShrink @UnknownReplace + @UnknownGrow Object ug1 = unmod; + @UnknownShrink Object us1 = unmod; + @UnknownReplace Object ur1 = unmod; + @UnknownGrow Object ug2 = unknown; + + // @Modifiable <: @Unmodifiable in every hierarchy + @Unmodifiable Object unmod1 = mod; + @UnknownModifiability Object unknown1 = mod; + + // @Unmodifiable !<: @Growable (top !<: bottom in Grow hierarchy) // :: error: (assignment) - @Modifiable Object m8 = unknown; + @Growable Object g2 = unmod; + + // @Unmodifiable !<: @Modifiable // :: error: (assignment) - @Modifiable Object m9 = unmod; + @Modifiable Object mod1 = unmod; } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 3d7a75dabd84..badfc5e39751 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -61,178 +61,243 @@ \sectionAndLabel{Modifiability annotations}{modifiability-annotations} -Figure~\ref{fig-modifiability-lattice} shows the type qualifiers of the -Modifiability Checker. +The Modifiability Checker uses \textbf{three independent 2-element +hierarchies}, one per capability. Every annotated type carries exactly +one qualifier from each hierarchy. % TODO: add a figure that follows the sturucture of other checker manuals. \begin{figure} \centering - \begin{minipage}{0.8\linewidth} + \begin{minipage}{0.85\linewidth} \begin{verbatim} - @UnknownModifiability - (@Unmodifiable) - / | \ - / | \ - @Growable @Shrinkable @Replaceable - | \ / \ / | - | x x | - | / \ / \ | - @GrowShrink @GrowReplace @ShrinkReplace - \ | / - \ | / - \ | / - @Modifiable + Grow hierarchy Shrink hierarchy Replace hierarchy + + @UnknownGrow @UnknownShrink @UnknownReplace + | | | + @Growable @Shrinkable @Replaceable \end{verbatim} \end{minipage} - \caption{Modifiability qualifier hierarchy.} + \caption{The three independent Modifiability qualifier hierarchies. + Each annotated type carries one qualifier from each hierarchy.} \label{fig-modifiability-lattice} \end{figure} -\begin{description} +Figure~\ref{fig-modifiability-lattice} shows the three hierarchies. +An assignment \code{T x = y} is valid only if the type of \code{y} is a +subtype of \code{T} in \emph{every} hierarchy simultaneously. For +example, \code{@Growable @UnknownShrink @UnknownReplace List} is a +subtype of \code{@UnknownGrow @UnknownShrink @UnknownReplace List} +(because \<@Growable> $\le$ \<@UnknownGrow>), but it is \emph{not} a +subtype of \code{@Growable @Shrinkable @UnknownReplace List} (because +\<@UnknownShrink> $\not\le$ \<@Shrinkable>). -\item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] -The checker does not know whether the -collection is modifiable or unmodifiable; it might be either. -The top qualifier in the hierarchy, and it is the default -qualifier that is applied to unannotated collection types. +\subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} -\item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] -The collection is definitely unmodifiable. The checker treats it -identically to \code{@UnknownModifiability}, but the different -name is useful for documentation purposes. +\begin{description} -\item[\refqualclass{checker/modifiability/qual}{Modifiable}] -Calling mutating operations such as \code{add}, \code{remove}, -\code{clear}, etc. on this collection will not result in throwing -\code{UnsupportedOperationException}. +\item[\refqualclass{checker/modifiability/qual}{UnknownGrow}] +The top qualifier in the Grow hierarchy. +The checker cannot determine whether this collection supports grow +operations. This is the default qualifier for unannotated types in the +Grow hierarchy. \item[\refqualclass{checker/modifiability/qual}{Growable}] -Calling grow operations such as \code{add}, \code{addAll}, etc. on this -collection will not result in throwing -\code{UnsupportedOperationException}. -Calling shrink or replace operations might or might not result in -throwing \code{UnsupportedOperationException}. +The bottom qualifier in the Grow hierarchy. +Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this +collection will not result in throwing \code{UnsupportedOperationException}. +No guarantees are made about shrink or replace operations. + +\item[\refqualclass{checker/modifiability/qual}{UnknownShrink}] +The top qualifier in the Shrink hierarchy. +The checker cannot determine whether this collection supports shrink +operations. This is the default qualifier for unannotated types in the +Shrink hierarchy. \item[\refqualclass{checker/modifiability/qual}{Shrinkable}] -Calling shrink operations such as \code{remove}, \code{clear}, etc. on this -collection will not result in throwing +The bottom qualifier in the Shrink hierarchy. +Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on +this collection will not result in throwing \code{UnsupportedOperationException}. -Calling grow or replace operations might or might not result in -throwing \code{UnsupportedOperationException}. +No guarantees are made about grow or replace operations. + +\item[\refqualclass{checker/modifiability/qual}{UnknownReplace}] +The top qualifier in the Replace hierarchy. +The checker cannot determine whether this collection supports replace +operations. This is the default qualifier for unannotated types in the +Replace hierarchy. \item[\refqualclass{checker/modifiability/qual}{Replaceable}] -Calling replace operations such as \code{set}, \code{replaceAll}, etc. on -this collection will not result in throwing +The bottom qualifier in the Replace hierarchy. +Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ +on this collection will not result in throwing \code{UnsupportedOperationException}. -Calling grow or shrink operations might or might not result in -throwing \code{UnsupportedOperationException}. - -\item[\refqualclass{checker/modifiability/qual}{GrowShrink}] -Calling grow and shrink operations on this collection will not result in -throwing \code{UnsupportedOperationException}. -Calling replace operations might or might not result in -throwing \code{UnsupportedOperationException}. - -\item[\refqualclass{checker/modifiability/qual}{GrowReplace}] -Calling grow and replace operations on this collection will not result in -throwing \code{UnsupportedOperationException}. -Calling shrink operations might or might not result in -throwing \code{UnsupportedOperationException}. - -\item[\refqualclass{checker/modifiability/qual}{ShrinkReplace}] -Calling shrink and replace operations on this collection will not result in -throwing \code{UnsupportedOperationException}. -Calling grow operations might or might not result in -throwing \code{UnsupportedOperationException}. +No guarantees are made about grow or shrink operations. \end{description} -\subsectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} - -There are types that have no methods for certain mutations. +\subsectionAndLabel{Combining capabilities}{modifiability-combining} -One example is \code{Set}. -The \code{Set} interface does not have replace methods, so -there is no difference between a replaceable \ and an unreplaceable \: -\code{@Modifiable Set} and \code{@GrowShrink Set} mean the same -thing. -For consistency, the Modifiability Checker treats all \code{Set}s as not replaceable. -\begin{itemize} - \item If a \code{Set} is annotated as \code{@Modifiable}, the - Modifiability Checker treats it as \code{@GrowShrink}. - \item If a \code{Set} is annotated as \code{@GrowReplace}, the - Modifiability Checker changes it to \code{@Growable}. - \item If a \code{Set} is annotated as \code{@ShrinkReplace}, the - Modifiability Checker changes it to \code{@Shrinkable}. - \item If a \code{Set} is annotated as \code{@UnknownModifiability}, the - checker still treats it as \code{@UnknownModifiability} (no change). -\end{itemize} +To express multiple capabilities, write one annotation per hierarchy: -Another example is \code{Iterator}. \code{Iterator} has the optional -\code{remove} method, but no growth or replacement operations. -For consistency, the Modifiability Checker treats all iterators as not -replaceable and not growable. Note that \code{ListIterator} is treated -normally because it has optional \code{add} and \code{set} operations. +\begin{Verbatim} + @Growable @Shrinkable List // can grow and shrink, replace unknown + @Growable @Replaceable List // can grow and replace, shrink unknown + @Growable @Shrinkable @Replaceable List // all three capabilities +\end{Verbatim} -Another example is \code{Map.Entry}. \code{Map.Entry} has the optional -\code{setValue} method. -The Modifiability Checker treats all \code{Map.Entry} objects as not growable and not shrinkable. +Because the three hierarchies are independent, subtyping checks each +hierarchy individually. A \code{@Growable @Shrinkable List} is a +subtype of \code{@Growable List} (Shrink information is dropped, Replace +defaults to \<@UnknownReplace> on both sides), but it is \emph{not} a +subtype of \code{@Growable @Shrinkable @Replaceable List} (the Replace +hierarchy differs). -\subsectionAndLabel{Polymorphic Types}{modifiability-polymorphic} +\subsectionAndLabel{Alias annotations}{modifiability-aliases} -A polymorphic annotations specifies that a method's return type has the -same modifiability as one or more formal parameter types (possibly -including the receiver). +For convenience, the Modifiability Checker provides four alias +annotations. They are not part of any hierarchy; the checker expands +each one into its constituent qualifiers when it appears in source code. \begin{description} + +\item[\refqualclass{checker/modifiability/qual}{Modifiable}] +Alias for \code{@Growable @Shrinkable @Replaceable}. +Calling any mutating operation on this collection will not throw +\code{UnsupportedOperationException}. + +\item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] +Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. +The collection is definitely unmodifiable: calling any mutating operation +always throws \code{UnsupportedOperationException}. + +\item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] +Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. +The checker cannot determine the modifiability of the collection. +Equivalent to \<@Unmodifiable>. + \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] - Full Preservation. Growability, shrinkability, and replacability of the - return type match those of whichever formal parameter is annotated as - \<@PolyModifiable>. - Used by strict wrappers like \code{Collections.synchronizedList}. -\item[\refqualclass{checker/modifiability/qual}{PolyShrink}] - Shrink Preservation. The return type has the same shrinkability as the - annotated formal parameter. The growability and shrinkability are - determined by non-polymorphic qualifiers from - Figure~\ref{fig-modifiability-lattice}. +Alias for \code{@PolyGrow @PolyShrink @PolyReplace} +(see Section~\ref{modifiability-polymorphic}). - If \code{@PolyShrink} is the only modifiability qualifier present on a return -type, the capabilities for Grow and Replace default to not guaranteed. The Shrink capability is dynamically determined by the -argument. +\end{description} + +\noindent +Examples using alias annotations: -For example, consider the method: \begin{Verbatim} - @PolyShrink Set keySet(@PolyShrink Map self) + @Modifiable List mod = new ArrayList<>(); // all three capabilities + @Unmodifiable List unmod = List.of("a"); // no capabilities + @UnknownModifiability List unknown = ...; // same as @Unmodifiable \end{Verbatim} -The resulting type of the returned set depends on the input map as follows: + +\subsectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} + +Some Java types lack methods for certain kinds of mutation. The +Modifiability Checker automatically removes capabilities that the type +cannot support. + +\paragraph{Set and Queue (except LinkedList).} +The \code{Set} interface has no replace methods (\code{set} is a +\code{List}-only operation). Similarly, \code{Queue} has no replace +methods in general. The Modifiability Checker therefore sets the +Replace hierarchy qualifier to \<@UnknownReplace> for any \code{Set} or +non-\code{LinkedList} \code{Queue}, regardless of what is written on the +type. For example: + +\begin{itemize} + \item \code{@Growable @Shrinkable @Replaceable Set} $\Rightarrow$ + treated as \code{@Growable @Shrinkable @UnknownReplace Set} + \item \code{@Growable @Replaceable Set} $\Rightarrow$ + treated as \code{@Growable @UnknownShrink @UnknownReplace Set} + \item \code{@Growable @Shrinkable Set} $\Rightarrow$ unchanged + (\<@UnknownReplace> is already the default) +\end{itemize} + +\paragraph{Map.Entry.} +\code{Map.Entry} has only the optional \code{setValue} method (a +replace operation) and no grow or shrink operations. The Modifiability +Checker therefore sets the Grow and Shrink hierarchy qualifiers to +\<@UnknownGrow> and \<@UnknownShrink> respectively. For example: + \begin{itemize} - \item If the map is \code{@Modifiable}, \code{@ShrinkReplace}, \code{@GrowShrink}, or \code{@Shrinkable} the returned set is - \code{@Shrinkable}. - \item If the map is \code{@Growable}, \code{@GrowReplace}, \code{@Replaceable} or \code{@Unmodifiable}, the returned set is - \code{@Unmodifiable}. + \item \code{Map.@Growable @Shrinkable @Replaceable Entry} $\Rightarrow$ + treated as \code{Map.@UnknownGrow @UnknownShrink @Replaceable Entry} + \item \code{Map.@Growable Entry} $\Rightarrow$ + treated as \code{Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry} \end{itemize} -If both a polymorphic qualifier and a non-polymorphic qualifier are specified on -a return type, the capabilities are additive. The non-polymorphic -qualifier defines the base capabilities (the ``floor'') guaranteed for the -return type, while the polymorphic qualifier preserves the specific shrink -capability from the argument. +\paragraph{Iterator.} +\code{Iterator} has only the optional \code{remove} method (a shrink +operation) and no grow or replace operations. (Note that +\code{ListIterator} is treated normally because it also has optional +\code{add} and \code{set} operations.) The Modifiability Checker +therefore sets the Grow and Replace hierarchy qualifiers to +\<@UnknownGrow> and \<@UnknownReplace>: + +\begin{itemize} + \item \code{@Growable @Shrinkable @Replaceable Iterator} $\Rightarrow$ + treated as \code{@UnknownGrow @Shrinkable @UnknownReplace Iterator} + \item \code{@Growable Iterator} $\Rightarrow$ + treated as \code{@UnknownGrow @UnknownShrink @UnknownReplace Iterator} +\end{itemize} + + +\subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} + +A polymorphic qualifier specifies that a method's return type has the +same qualifier (in some hierarchy) as a formal parameter type (possibly +the receiver). + +\begin{description} + +\item[\refqualclass{checker/modifiability/qual}{PolyGrow}] +Polymorphic qualifier for the Grow hierarchy. +The return type's Grow qualifier matches the Grow qualifier of whichever +formal parameter is annotated with \<@PolyGrow>. + +\item[\refqualclass{checker/modifiability/qual}{PolyShrink}] +Polymorphic qualifier for the Shrink hierarchy. +The return type's Shrink qualifier matches the Shrink qualifier of +whichever formal parameter is annotated with \<@PolyShrink>. +Useful for methods such as \code{Map.keySet()} that strip Grow and +Replace but preserve Shrink. -For example, a method annotated with \code{@PolyShrink @Growable} returns -a \code{@GrowShrink} collection if the argument is \code{@Shrinkable}, -but returns a \code{@Growable} collection if the argument is -\code{@Unmodifiable}. If a return type is annotated with both -\code{@PolyShrink} and \code{@Modifiable}, the \code{@PolyShrink} -annotation is redundant because \code{@Modifiable} already guarantees -shrinkability. +\item[\refqualclass{checker/modifiability/qual}{PolyReplace}] +Polymorphic qualifier for the Replace hierarchy. +The return type's Replace qualifier matches the Replace qualifier of +whichever formal parameter is annotated with \<@PolyReplace>. + +\item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] +Alias for \code{@PolyGrow @PolyShrink @PolyReplace}. +Preserves all three capabilities simultaneously. +Use on methods such as \code{Collections.synchronizedList} that are pure +wrappers and do not change modifiability. \end{description} +\noindent +Because the three poly qualifiers are independent, you can mix +polymorphic and concrete qualifiers across hierarchies. For example: + +\begin{Verbatim} + // Shrink is preserved from the map; Grow and Replace are not. + @PolyShrink Set keySet(@PolyShrink Map map) +\end{Verbatim} + +If the map is \code{@Growable @Shrinkable @UnknownReplace}, then +\code{keySet} returns \code{@UnknownGrow @Shrinkable @UnknownReplace Set} +(after the Modifiability Checker also strips Replace from the \code{Set} +return type, which was already \<@UnknownReplace>). + +If the map is \code{@Growable @UnknownShrink @UnknownReplace}, then +\code{keySet} returns \code{@UnknownGrow @UnknownShrink @UnknownReplace Set}. + + \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} -The Modifiability Checker supports one annotation that specify method -behavior. This is a declaration annotation, not a type annotation: it +The Modifiability Checker supports one annotation that specifies method +behavior. This is a declaration annotation, not a type annotation: it applies to the method itself rather than to some particular type. \begin{description} @@ -240,7 +305,7 @@ \item[\refqualclass{checker/modifiability/qual}{ThrowsUOE}] This is a declaration annotation (not a type qualifier) that is applied to method and constructor declarations. It indicates that the method always throws -\code{UnsupportedOperationException}. (Therefore, any code that call it is +\code{UnsupportedOperationException}. (Therefore, any code that calls it is erroneous.) The Modifiability Checker issues an error at every call to a method annotated with \code{@ThrowsUOE}. @@ -257,45 +322,53 @@ class Demo { void basicUsage() { + // @Modifiable is an alias for @Growable @Shrinkable @Replaceable @Modifiable List mod = new ArrayList<>(); - mod.add("a"); // OK: @Modifiable implies G=1, S=1, R=1 - mod.remove("a"); // OK - mod.set(0, "b"); // OK + mod.add("a"); // OK: @Growable guarantees add() works + mod.remove("a"); // OK: @Shrinkable guarantees remove() works + mod.set(0, "b"); // OK: @Replaceable guarantees set() works + // @Unmodifiable is an alias for @UnknownGrow @UnknownShrink @UnknownReplace @Unmodifiable List unmod = List.of("a", "b"); - unmod.add("c"); // Error: receiver requires @Growable, found @Unmodifiable - unmod.get(0); // OK: read-only access matches any capability + unmod.add("c"); // Error: add() requires @Growable, got @UnknownGrow + unmod.get(0); // OK: read-only access needs no capability } void fineGrainedPermissions(@Growable List g, @Shrinkable List s, @Replaceable List r) { - // Growable list allows adding elements (G=1) + // Growable list allows adding elements g.add("a"); // OK - g.remove("a"); // Error: remove requires @Shrinkable - g.set(0, "b"); // Error: set requires @Replaceable + g.remove("a"); // Error: remove() requires @Shrinkable + g.set(0, "b"); // Error: set() requires @Replaceable - // Shrinkable list allows removing elements (S=1) + // Shrinkable list allows removing elements s.remove("a"); // OK - s.add("a"); // Error: add requires @Growable + s.add("a"); // Error: add() requires @Growable - // Replaceable list allows updating elements (R=1) + // Replaceable list allows updating elements r.set(0, "b"); // OK - r.add("b"); // Error: add requires @Growable + r.add("b"); // Error: add() requires @Growable } - void combinedPermissions(@GrowReplace Map map) { - // Map.put requires both Grow and Replace capabilities (G=1, R=1) - map.put("key", "value"); // OK: receiver is @GrowReplace + void combinedPermissions(@Growable @Replaceable Map map) { + // Map.put requires both Grow and Replace capabilities + map.put("key", "value"); // OK + + // Map.remove requires Shrink capability + map.remove("key"); // Error: @UnknownShrink (default) !<: @Shrinkable + } - // Map.remove requires Shrink capability (S=1) - map.remove("key"); // Error: receiver is @GrowReplace, - // but remove requires @Shrinkable + // @PolyModifiable preserves all three capabilities + @PolyModifiable List wrap(@PolyModifiable List list) { + return list; } - // Polymorphism preserves capabilities - @PolyModifiable List process(@PolyModifiable List list) { - return list.subList(0, 1); + void testPoly(@Modifiable List mod, @Growable @Shrinkable List gs) { + @Modifiable List m = wrap(mod); // OK + @Growable @Shrinkable List gs2 = wrap(gs); // OK + // :: error: (assignment) + @Modifiable List bad = wrap(gs); // Error: gs has @UnknownReplace } } \end{Verbatim} @@ -303,7 +376,8 @@ % LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap % LocalWords: toArray addAll removeAll retainAll replaceAll setValue -% LocalWords: UnknownModifiability Growable GrowShrink GrowReplace -% LocalWords: ShrinkReplace growable ListIterator PolyModifiable addFirst -% LocalWords: synchronizedList PolyShrink ThrowsUOE addLast unreplaceable -% LocalWords: Growability shrinkability replacability growability +% LocalWords: UnknownModifiability Growable UnknownGrow UnknownShrink +% LocalWords: UnknownReplace growable ListIterator PolyModifiable addFirst +% LocalWords: synchronizedList PolyShrink PolyGrow PolyReplace ThrowsUOE +% LocalWords: addLast unreplaceable Growability shrinkability replacability +% LocalWords: growability Shrinkable Replaceable From 5368e9b05ecfb22f022b9d39d47b03c1f1671bf1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 19 Feb 2026 16:53:40 -0800 Subject: [PATCH 045/308] add alias handling. removed composite types in testing --- .../ModifiabilityAnnotatedTypeFactory.java | 60 ++++ .../modifiability/PolyModifiableTest.java | 83 +++-- checker/tests/modifiability/SetTest.java | 285 +++++++++++------- 3 files changed, 284 insertions(+), 144 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 6c7ba559ce84..c42b38f8b59d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -136,6 +136,47 @@ protected void postInit() { this.POLY_MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyModifiable.class); } + /** + * Expands alias annotations ({@code @Modifiable}, {@code @Unmodifiable}, + * {@code @UnknownModifiability}, {@code @PolyModifiable}) found on the element's underlying + * {@link TypeMirror}. These alias annotations are not supported qualifiers, so they are dropped + * by the standard pipeline. This override recovers them from the raw type annotations. + * + *

This is critical for method parameters, whose types are resolved via the element-based path + * (not the tree-based path) when initializing the flow-sensitive store. + */ + @Override + public void addComputedTypeAnnotations(Element element, AnnotatedTypeMirror type) { + super.addComputedTypeAnnotations(element, type); + expandAliasesFromTypeMirror(element.asType(), type); + } + + /** + * Checks the given {@link TypeMirror} for alias annotations and expands them onto the given + * {@link AnnotatedTypeMirror}. + */ + private void expandAliasesFromTypeMirror(TypeMirror typeMirror, AnnotatedTypeMirror type) { + for (AnnotationMirror anno : typeMirror.getAnnotationMirrors()) { + if (AnnotationUtils.areSame(anno, MODIFIABLE)) { + type.replaceAnnotation(GROWABLE); + type.replaceAnnotation(SHRINKABLE); + type.replaceAnnotation(REPLACEABLE); + return; + } else if (AnnotationUtils.areSame(anno, UNMODIFIABLE) + || AnnotationUtils.areSame(anno, UNKNOWN_MODIFIABILITY)) { + type.replaceAnnotation(UNKNOWN_GROW); + type.replaceAnnotation(UNKNOWN_SHRINK); + type.replaceAnnotation(UNKNOWN_REPLACE); + return; + } else if (AnnotationUtils.areSame(anno, POLY_MODIFIABLE)) { + type.replaceAnnotation(POLY_GROW); + type.replaceAnnotation(POLY_SHRINK); + type.replaceAnnotation(POLY_REPLACE); + return; + } + } + } + @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( @@ -208,8 +249,27 @@ private void expandAliases( break; } } + } + + @Override + public Void visitAnnotatedType(AnnotatedTypeTree node, AnnotatedTypeMirror type) { + expandAliases(TreeUtils.annotationsFromTree(node), type); return super.visitAnnotatedType(node, type); } + + /** + * Handles alias expansion for variable declarations (local variables, fields, and parameters). + * The tree annotator is called with the {@link VariableTree}, not the inner {@link + * AnnotatedTypeTree}, so {@link #visitAnnotatedType} does not fire for these cases. + */ + @Override + public Void visitVariable(VariableTree node, AnnotatedTypeMirror type) { + Tree typeTree = node.getType(); + if (typeTree instanceof AnnotatedTypeTree) { + expandAliases(TreeUtils.annotationsFromTree((AnnotatedTypeTree) typeTree), type); + } + return super.visitVariable(node, type); + } } @Override diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index f6f53add9e6d..96c3f5ef984c 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,17 +1,19 @@ import java.util.List; -import org.checkerframework.checker.modifiability.qual.GrowReplace; -import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.ShrinkReplace; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +/** + * Tests @PolyModifiable, which expands to @PolyGrow @PolyShrink @PolyReplace and thus preserves all + * three capabilities independently. + */ public class PolyModifiableTest { - // A simple polymorphic method + + /** A simple polymorphic identity method that preserves all three capabilities. */ @PolyModifiable List identity(@PolyModifiable List x) { return x; @@ -19,9 +21,9 @@ List identity(@PolyModifiable List x) { void testPoly( @Modifiable List mod, - @GrowShrink List gs, - @GrowReplace List gr, - @ShrinkReplace List sr, + @Growable @Shrinkable List gs, // Grow+Shrink; Replace = UnknownReplace (default) + @Growable @Replaceable List gr, // Grow+Replace; Shrink = UnknownShrink + @Shrinkable @Replaceable List sr, // Shrink+Replace; Grow = UnknownGrow @Growable List g, @Shrinkable List s, @Replaceable List r, @@ -29,79 +31,94 @@ void testPoly( @Unmodifiable List unmod) { // ============================================================ - // Identity on Modifiable (Bottom - 111) + // Identity on @Modifiable (G+S+R) // ============================================================ - @Modifiable List m1 = identity(mod); // OK - @UnknownModifiability List m2 = identity(mod); // OK + @Modifiable List m1 = identity(mod); // OK: poly resolves to G+S+R + @UnknownModifiability List m2 = identity(mod); // OK: G+S+R <: top in all // ============================================================ - // Identity on GrowShrink (110) + // Identity on @Growable @Shrinkable (G+S, R=UnknownReplace) // ============================================================ - @GrowShrink List gs1 = identity(gs); // OK + @Growable + @Shrinkable + List gs1 = identity(gs); // OK // :: error: (assignment) - @Modifiable List gs2 = identity(gs); // Error: GS (110) !<: Mod (111) - @Growable List gs3 = identity(gs); // OK: GS (110) <: G (100) - @Shrinkable List gs4 = identity(gs); // OK: GS (110) <: S (010) + @Modifiable List gs2 = identity(gs); // Error: R=Unknown !<: Replaceable + @Growable List gs3 = identity(gs); // OK: G+S+UnknownR <: G + @Shrinkable List gs4 = identity(gs); // OK: G+S+UnknownR <: S // :: error: (assignment) - @Replaceable List gs5 = identity(gs); // Error: GS (110) !<: R (001) + @Replaceable List gs5 = identity(gs); // Error: UnknownR !<: Replaceable // ============================================================ - // Identity on GrowReplace (101) + // Identity on @Growable @Replaceable (G+R, S=UnknownShrink) // ============================================================ - @GrowReplace List gr1 = identity(gr); // OK + @Growable + @Replaceable + List gr1 = identity(gr); // OK // :: error: (assignment) - @Modifiable List gr2 = identity(gr); // Error + @Modifiable List gr2 = identity(gr); // Error: S=Unknown !<: Shrinkable @Growable List gr3 = identity(gr); // OK // :: error: (assignment) - @Shrinkable List gr4 = identity(gr); // Error + @Shrinkable List gr4 = identity(gr); // Error: S=Unknown @Replaceable List gr5 = identity(gr); // OK // ============================================================ - // Identity on ShrinkReplace (011) + // Identity on @Shrinkable @Replaceable (S+R, G=UnknownGrow) // ============================================================ - @ShrinkReplace List sr1 = identity(sr); // OK + @Shrinkable + @Replaceable + List sr1 = identity(sr); // OK // :: error: (assignment) - @Modifiable List sr2 = identity(sr); // Error + @Modifiable List sr2 = identity(sr); // Error: G=Unknown !<: Growable // :: error: (assignment) - @Growable List sr3 = identity(sr); // Error + @Growable List sr3 = identity(sr); // Error: G=Unknown @Shrinkable List sr4 = identity(sr); // OK @Replaceable List sr5 = identity(sr); // OK // ============================================================ - // Identity on Growable (100) + // Identity on @Growable (G only; S=Unknown, R=Unknown) // ============================================================ @Growable List g1 = identity(g); // OK // :: error: (assignment) - @GrowShrink List g2 = identity(g); // Error + @Growable + @Shrinkable + List g2 = identity(g); // Error: S=Unknown // :: error: (assignment) - @Modifiable List g3 = identity(g); // Error + @Modifiable List g3 = identity(g); // Error: S=Unknown, R=Unknown // ============================================================ - // Identity on Shrinkable (010) + // Identity on @Shrinkable (S only; G=Unknown, R=Unknown) // ============================================================ @Shrinkable List s1 = identity(s); // OK // :: error: (assignment) - @GrowShrink List s2 = identity(s); // Error + @Growable + @Shrinkable + List s2 = identity(s); // Error: G=Unknown // :: error: (assignment) @Modifiable List s3 = identity(s); // Error + // ============================================================ - // Identity on Replaceable (001) + // Identity on @Replaceable (R only; G=Unknown, S=Unknown) // ============================================================ @Replaceable List r1 = identity(r); // OK // :: error: (assignment) - @GrowReplace List r2 = identity(r); // Error + @Growable + @Replaceable + List r2 = identity(r); // Error: G=Unknown // :: error: (assignment) @Modifiable List r3 = identity(r); // Error + // ============================================================ - // Identity on UnknownModifiability (000 / Top) + // Identity on @UnknownModifiability (all tops) // ============================================================ @UnknownModifiability List u1 = identity(unknown); // OK // :: error: (assignment) @Modifiable List u2 = identity(unknown); // Error // :: error: (assignment) @Growable List u3 = identity(unknown); // Error + // ============================================================ - // Identity on Unmodifiable (000 / Top alias) + // Identity on @Unmodifiable (alias for all tops) // ============================================================ @Unmodifiable List unmod1 = identity(unmod); // OK // :: error: (assignment) diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 6d1ee84c90e5..6f26f1dc12fb 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -2,148 +2,211 @@ import java.util.Map; import java.util.Queue; import java.util.Set; -import org.checkerframework.checker.modifiability.qual.GrowReplace; -import org.checkerframework.checker.modifiability.qual.GrowShrink; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.ShrinkReplace; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; - +import org.checkerframework.checker.modifiability.qual.UnknownGrow; +import org.checkerframework.checker.modifiability.qual.UnknownReplace; +import org.checkerframework.checker.modifiability.qual.UnknownShrink; + +/** + * Tests structural capability removal for Set, Queue, Map.Entry, and Iterator. + * + *

Set / Queue: TypeAnnotator removes the Replace capability (sets Replace to @UnknownReplace). + * Map.Entry: TypeAnnotator removes Grow and Shrink capabilities. Iterator: TypeAnnotator removes + * Grow and Replace capabilities. + */ class SetTest { + // ========================================================== - // Set: No Replace methods. Implicitly @Replaceable. + // Set: Replace capability is always removed by the TypeAnnotator. + // Effective types have Replace = @UnknownReplace. // ========================================================== - void testSetAssignment( - @UnknownModifiability Set unknown, + void testSetReplaceRemoved( @Replaceable Set replaceable, - @Shrinkable Set shrinkable, - @Modifiable Set modifiable, - @Growable Set growable, - @GrowReplace Set growReplace, - @ShrinkReplace Set shrinkReplace, - @GrowShrink Set growShrink) { - - // Unknown <-> Replaceable - @Replaceable Set s1 = unknown; // Valid: Unknown -> Replaceable (Implicit R) - @UnknownModifiability Set s2 = replaceable; // Valid: Replaceable <: Unknown - - // Shrinkable <-> ShrinkReplace - @ShrinkReplace Set s3 = shrinkable; // Valid: Shrinkable -> ShrinkReplace (Implicit R) - @Shrinkable Set s4 = shrinkReplace; // Valid: ShrinkReplace <: Shrinkable - - // GrowShrink <-> Modifiable - @Modifiable Set s5 = growShrink; // Valid: GrowShrink -> Modifiable (Implicit R) - @GrowShrink Set s6 = modifiable; // Valid: Modifiable <: GrowShrink - - // Growable <-> GrowReplace - @GrowReplace Set s7 = growable; // Valid: Growable -> GrowReplace (Implicit R) - @Growable Set s8 = growReplace; // Valid: GrowReplace <: Growable + @Growable @Replaceable Set growReplace, + @Shrinkable @Replaceable Set shrinkReplace, + @Growable @Shrinkable @Replaceable Set modifiable) { + + // After TypeAnnotator, all Set Replace qualifiers become @UnknownReplace. + // So @Replaceable Set, @Growable @Replaceable Set, etc. lose their Replaceable. + + // @Replaceable Set → Grow=Unknown, Shrink=Unknown, Replace=Unknown (R removed) + // @Growable @Replaceable Set → Grow=Growable, Shrink=Unknown, Replace=Unknown (R removed) + @Growable Set g1 = growReplace; // OK: Grow still intact + // :: error: (assignment) + @Replaceable Set r1 = growReplace; // Error: R removed → UnknownReplace !<: Replaceable + + // @Growable @Shrinkable @Replaceable Set → Grow=G, Shrink=S, Replace=Unknown (R removed) + // effectively the same as @Growable @Shrinkable Set + @Growable + @Shrinkable + Set gs1 = modifiable; // OK + // :: error: (assignment) + @Replaceable Set r2 = modifiable; // Error: R removed } - void testQueueAssignment( - @Modifiable Queue modifiable, @GrowShrink Queue growShrink) { + void testSetGrowShrinkPreserved( + @UnknownGrow @UnknownShrink @UnknownReplace Set unknown, + @Growable Set growable, + @Shrinkable Set shrinkable, + @Growable @Shrinkable Set gs) { + + // Top in all hierarchies: accepts everything + @UnknownGrow + @UnknownShrink + @UnknownReplace + Set u1 = growable; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Set u2 = shrinkable; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Set u3 = gs; + + // @Growable Set: Grow=G, Shrink=Unknown, Replace=Unknown + @Growable Set g1 = gs; // OK: G+S <: G + // :: error: (assignment) + @Growable Set g2 = shrinkable; // Error: S only, no G + + // @Shrinkable Set: Grow=Unknown, Shrink=S, Replace=Unknown + @Shrinkable Set s1 = gs; // OK: G+S <: S + // :: error: (assignment) + @Shrinkable Set s2 = growable; // Error: G only, no S + } - // GrowShrink <-> Modifiable - @Modifiable Queue q5 = growShrink; // Valid: GrowShrink -> Modifiable (Implicit R) - @GrowShrink Queue q6 = modifiable; // Valid: Modifiable <: GrowShrink + void testQueueLikeSet( + @Modifiable Queue modifiable, @Growable @Shrinkable Queue gs) { + // Queue also has Replace removed (same rule as Set). + @Growable + @Shrinkable + Queue q1 = modifiable; // OK: modifiable <: gs (R removed from both) + @Growable + @Shrinkable + @Replaceable + Queue q2 = gs; // OK: same after TypeAnnotator } // ========================================================== - // Map.Entry: Drops Grow (100) and Shrink (010) capabilities. - // Effectively supports only Replace (001). + // Map.Entry: Grow and Shrink capabilities are both removed. + // Effective types only retain the Replace hierarchy annotation. // ========================================================== void testEntryAssignment( - // Group 1: No Replace Bit (Result: @UnknownModifiability) - Map.@UnknownModifiability Entry unknown, + // No Replace bit: effective Replace = @UnknownReplace after G+S removal + Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry unknown, Map.@Growable Entry growable, Map.@Shrinkable Entry shrinkable, - Map.@GrowShrink Entry growShrink, - - // Group 2: Has Replace Bit (Result: @Replaceable) + Map.@Growable @Shrinkable Entry gs, + // Has Replace bit: retains @Replaceable after G+S removal Map.@Replaceable Entry replaceable, - Map.@GrowReplace Entry growReplace, - Map.@ShrinkReplace Entry shrinkReplace, - Map.@Modifiable Entry modifiable) { - - // Group 1: Unknown = Growable = Shrinkable = GrowShrink -> - // @UnknownModifiability - // These types lose G and S capabilities, degrading to Unknown (000). - - Map.@UnknownModifiability Entry u1 = growable; - Map.@UnknownModifiability Entry u2 = shrinkable; - Map.@UnknownModifiability Entry u3 = growShrink; - - // Since they are all @Unknown (000), they can be assigned to each other - Map.@Growable Entry g1 = unknown; + Map.@Growable @Replaceable Entry growReplace, + Map.@Shrinkable @Replaceable Entry shrinkReplace, + Map.@Growable @Shrinkable @Replaceable Entry modifiable) { + + // Group 1: entries without Replace bit become @UnknownGrow @UnknownShrink @UnknownReplace + // (G and S are removed → both become Unknown; R was already Unknown) + Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u1 = growable; + Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u2 = shrinkable; + Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u3 = gs; + // After TypeAnnotator they are all effectively @UnknownGrow @UnknownShrink @UnknownReplace, + // so assignments between them are valid. + Map.@Growable Entry g1 = unknown; // OK: same effective type Map.@Shrinkable Entry s1 = unknown; - Map.@GrowShrink Entry gs1 = unknown; - Map.@Growable Entry g2 = growShrink; - - // Group 2: Replaceable = GrowReplace = ShrinkReplace = Modifiable -> - // @Replaceable - // These types retain R (001) but lose G and S, degrading to Replaceable. - - Map.@Replaceable Entry r1 = growReplace; - Map.@Replaceable Entry r2 = shrinkReplace; - Map.@Replaceable Entry r3 = modifiable; - - // Since they are all @Replaceable (001), they can be assigned to each other - Map.@GrowReplace Entry gr1 = replaceable; - Map.@ShrinkReplace Entry sr1 = replaceable; - Map.@Modifiable Entry m1 = replaceable; - - Map.@ShrinkReplace Entry sr2 = growReplace; - Map.@GrowReplace Entry gr2 = modifiable; + Map.@Growable Entry g2 = gs; // OK + + // Group 2: entries with Replace bit become @UnknownGrow @UnknownShrink @Replaceable + // (G and S are removed; R=Replaceable is preserved) + Map.@UnknownGrow @UnknownShrink @Replaceable Entry r1 = growReplace; + Map.@UnknownGrow @UnknownShrink @Replaceable Entry r2 = shrinkReplace; + Map.@UnknownGrow @UnknownShrink @Replaceable Entry r3 = modifiable; + // After TypeAnnotator they are all effectively @UnknownGrow @UnknownShrink @Replaceable. + Map.@Growable @Replaceable Entry gr1 = replaceable; // OK: same effective type + Map.@Shrinkable @Replaceable Entry sr1 = replaceable; + Map.@Growable @Shrinkable @Replaceable Entry m1 = replaceable; + Map.@Shrinkable @Replaceable Entry sr2 = growReplace; + Map.@Growable @Replaceable Entry gr2 = modifiable; + + // Cross-group: Replace !<: UnknownReplace (wrong direction) is NOT an error — + // but UnknownReplace !<: Replaceable IS an error: + // :: error: (assignment) + Map.@Replaceable Entry bad1 = growable; // Error: R=Unknown !<: Replaceable } // ========================================================== - // Iterator: No Grow/Replace methods. Implicitly @GrowReplace. + // Iterator: Grow and Replace capabilities are both removed. + // Effective types only retain the Shrink hierarchy annotation. // ========================================================== void testIteratorAssignment( - @UnknownModifiability Iterator unknown, + // No Shrink bit: effective after G+R removal → @UnknownGrow @UnknownShrink @UnknownReplace + @UnknownGrow @UnknownShrink @UnknownReplace Iterator unknown, @Growable Iterator growable, @Replaceable Iterator replaceable, - @GrowReplace Iterator growReplace, + @Growable @Replaceable Iterator growReplace, + // Has Shrink bit: effective after G+R removal → @UnknownGrow @Shrinkable @UnknownReplace @Shrinkable Iterator shrinkable, - @GrowShrink Iterator growShrink, - @ShrinkReplace Iterator shrinkReplace, - @Modifiable Iterator modifiable) { - - // Group 1: Unknown = Growable = Replaceable = GrowReplace - // All these imply @GrowReplace (1?1), so they are effectively equal and - // assignable. - - @GrowReplace Iterator gr1 = unknown; - @GrowReplace Iterator gr2 = growable; - @GrowReplace Iterator gr3 = replaceable; - @GrowReplace Iterator gr4 = growReplace; - - @UnknownModifiability Iterator u1 = growReplace; - @Growable Iterator g1 = growReplace; + @Growable @Shrinkable Iterator growShrink, + @Shrinkable @Replaceable Iterator shrinkReplace, + @Growable @Shrinkable @Replaceable Iterator modifiable) { + + // Group 1: No Shrink bit → effective @UnknownGrow @UnknownShrink @UnknownReplace + @UnknownGrow + @UnknownShrink + @UnknownReplace + Iterator u1 = growable; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Iterator u2 = replaceable; + @UnknownGrow + @UnknownShrink + @UnknownReplace + Iterator u3 = growReplace; + // They are all effectively the same type after TypeAnnotator: + @Growable Iterator g1 = unknown; @Replaceable Iterator r1 = growReplace; - - @Growable Iterator g2 = unknown; - @Replaceable Iterator r2 = growable; - - // Group 2: Shrinkable = GrowShrink = ShrinkReplace = Modifiable - // All these imply @Modifiable (111) because they have the S bit, - // and Iterator adds G and R bits. (010 + 101 = 111). - - @Modifiable Iterator m1 = shrinkable; - @Modifiable Iterator m2 = growShrink; - @Modifiable Iterator m3 = shrinkReplace; - @Modifiable Iterator m4 = modifiable; - - @Shrinkable Iterator s1 = modifiable; - @GrowShrink Iterator gs1 = modifiable; - @ShrinkReplace Iterator sr1 = modifiable; - - @GrowShrink Iterator gs2 = shrinkable; - @ShrinkReplace Iterator sr2 = growShrink; + @Growable + @Replaceable + Iterator gr1 = replaceable; + + // Group 2: Has Shrink bit → effective @UnknownGrow @Shrinkable @UnknownReplace + @UnknownGrow + @Shrinkable + @UnknownReplace + Iterator s1 = shrinkable; + @UnknownGrow + @Shrinkable + @UnknownReplace + Iterator s2 = growShrink; + @UnknownGrow + @Shrinkable + @UnknownReplace + Iterator s3 = shrinkReplace; + @UnknownGrow + @Shrinkable + @UnknownReplace + Iterator s4 = modifiable; + // Cross-assignments within group 2: + @Shrinkable Iterator sh1 = modifiable; + @Growable + @Shrinkable + Iterator gs1 = modifiable; + @Shrinkable + @Replaceable + Iterator sr1 = modifiable; + @Growable + @Shrinkable + Iterator gs2 = shrinkable; + @Shrinkable + @Replaceable + Iterator sr2 = growShrink; + + // :: error: (assignment) + @Shrinkable Iterator bad1 = growable; // Error: Shrink=Unknown !<: Shrinkable } } From 1f5073162d17bb01e145311a59a04fbbc0c4e461 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 20 Feb 2026 22:49:25 -0800 Subject: [PATCH 046/308] partially done with having individual checker for grow, shrink, and replace. --- .../ModifiabilityAnnotatedTypeFactory.java | 351 ------------------ .../modifiability/ModifiabilityChecker.java | 41 +- .../grow/GrowAnnotatedTypeFactory.java | 115 ++++++ .../modifiability/grow/GrowChecker.java | 34 ++ .../GrowVisitor.java} | 12 +- .../modifiability/grow/messages.properties | 1 + .../replace/ReplaceAnnotatedTypeFactory.java | 125 +++++++ .../modifiability/replace/ReplaceChecker.java | 34 ++ .../modifiability/replace/ReplaceVisitor.java | 45 +++ .../modifiability/replace/messages.properties | 1 + .../shrink/ShrinkAnnotatedTypeFactory.java | 106 ++++++ .../modifiability/shrink/ShrinkChecker.java | 34 ++ .../modifiability/shrink/ShrinkVisitor.java | 45 +++ .../modifiability/shrink/messages.properties | 1 + checker/tests/modifiability/SetTest.java | 10 +- 15 files changed, 574 insertions(+), 381 deletions(-) delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java rename checker/src/main/java/org/checkerframework/checker/modifiability/{ModifiabilityVisitor.java => grow/GrowVisitor.java} (85%) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java deleted file mode 100644 index c42b38f8b59d..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ /dev/null @@ -1,351 +0,0 @@ -package org.checkerframework.checker.modifiability; - -import com.sun.source.tree.AnnotatedTypeTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Types; -import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyGrow; -import org.checkerframework.checker.modifiability.qual.PolyModifiable; -import org.checkerframework.checker.modifiability.qual.PolyReplace; -import org.checkerframework.checker.modifiability.qual.PolyShrink; -import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownGrow; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; -import org.checkerframework.checker.modifiability.qual.UnknownReplace; -import org.checkerframework.checker.modifiability.qual.UnknownShrink; -import org.checkerframework.checker.modifiability.qual.Unmodifiable; -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.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.TypeAnnotator; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.TreeUtils; - -/** The type factory for the Modifiability Checker. */ -public class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - - /** The erased {@code java.util.Set} type. */ - private final TypeMirror setErasure; - - /** The erased {@code java.util.Queue} type. */ - private final TypeMirror queueErasure; - - /** The erased {@code java.util.LinkedList} type. */ - private final TypeMirror linkedListErasure; - - /** The erased {@code java.util.Map.Entry} type. */ - private final TypeMirror mapEntryErasure; - - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - - // ── Hierarchy qualifiers (9 total: 3 tops + 3 bottoms + 3 poly) ────────── - - /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ - private AnnotationMirror UNKNOWN_GROW; - - /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ - private AnnotationMirror UNKNOWN_SHRINK; - - /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ - private AnnotationMirror UNKNOWN_REPLACE; - - /** The {@code @}{@link Growable} qualifier (bottom of Grow hierarchy). */ - private AnnotationMirror GROWABLE; - - /** The {@code @}{@link Shrinkable} qualifier (bottom of Shrink hierarchy). */ - private AnnotationMirror SHRINKABLE; - - /** The {@code @}{@link Replaceable} qualifier (bottom of Replace hierarchy). */ - private AnnotationMirror REPLACEABLE; - - /** The {@code @}{@link PolyGrow} qualifier. */ - private AnnotationMirror POLY_GROW; - - /** The {@code @}{@link PolyShrink} qualifier. */ - private AnnotationMirror POLY_SHRINK; - - /** The {@code @}{@link PolyReplace} qualifier. */ - private AnnotationMirror POLY_REPLACE; - - // ── Alias qualifiers (not in hierarchy; expanded by the tree annotator) ── - - /** The {@code @}{@link Modifiable} alias annotation. */ - private AnnotationMirror MODIFIABLE; - - /** The {@code @}{@link Unmodifiable} alias annotation. */ - private AnnotationMirror UNMODIFIABLE; - - /** The {@code @}{@link UnknownModifiability} alias annotation. */ - private AnnotationMirror UNKNOWN_MODIFIABILITY; - - /** The {@code @}{@link PolyModifiable} alias annotation. */ - private AnnotationMirror POLY_MODIFIABLE; - - @SuppressWarnings("this-escape") - public ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker); - // Cache type erasures. - Types types = getProcessingEnv().getTypeUtils(); - this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); - this.queueErasure = types.erasure(getElementUtils().getTypeElement("java.util.Queue").asType()); - this.linkedListErasure = - types.erasure(getElementUtils().getTypeElement("java.util.LinkedList").asType()); - this.mapEntryErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); - this.iteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); - - postInit(); - } - - @Override - protected void postInit() { - super.postInit(); - // Initialize annotation mirrors after the hierarchy is established. - this.UNKNOWN_GROW = AnnotationBuilder.fromClass(getElementUtils(), UnknownGrow.class); - this.UNKNOWN_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), UnknownShrink.class); - this.UNKNOWN_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), UnknownReplace.class); - this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); - this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); - this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); - this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); - this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); - this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); - this.MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Modifiable.class); - this.UNMODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), Unmodifiable.class); - this.UNKNOWN_MODIFIABILITY = - AnnotationBuilder.fromClass(getElementUtils(), UnknownModifiability.class); - this.POLY_MODIFIABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyModifiable.class); - } - - /** - * Expands alias annotations ({@code @Modifiable}, {@code @Unmodifiable}, - * {@code @UnknownModifiability}, {@code @PolyModifiable}) found on the element's underlying - * {@link TypeMirror}. These alias annotations are not supported qualifiers, so they are dropped - * by the standard pipeline. This override recovers them from the raw type annotations. - * - *

This is critical for method parameters, whose types are resolved via the element-based path - * (not the tree-based path) when initializing the flow-sensitive store. - */ - @Override - public void addComputedTypeAnnotations(Element element, AnnotatedTypeMirror type) { - super.addComputedTypeAnnotations(element, type); - expandAliasesFromTypeMirror(element.asType(), type); - } - - /** - * Checks the given {@link TypeMirror} for alias annotations and expands them onto the given - * {@link AnnotatedTypeMirror}. - */ - private void expandAliasesFromTypeMirror(TypeMirror typeMirror, AnnotatedTypeMirror type) { - for (AnnotationMirror anno : typeMirror.getAnnotationMirrors()) { - if (AnnotationUtils.areSame(anno, MODIFIABLE)) { - type.replaceAnnotation(GROWABLE); - type.replaceAnnotation(SHRINKABLE); - type.replaceAnnotation(REPLACEABLE); - return; - } else if (AnnotationUtils.areSame(anno, UNMODIFIABLE) - || AnnotationUtils.areSame(anno, UNKNOWN_MODIFIABILITY)) { - type.replaceAnnotation(UNKNOWN_GROW); - type.replaceAnnotation(UNKNOWN_SHRINK); - type.replaceAnnotation(UNKNOWN_REPLACE); - return; - } else if (AnnotationUtils.areSame(anno, POLY_MODIFIABLE)) { - type.replaceAnnotation(POLY_GROW); - type.replaceAnnotation(POLY_SHRINK); - type.replaceAnnotation(POLY_REPLACE); - return; - } - } - } - - @Override - protected Set> createSupportedTypeQualifiers() { - return new LinkedHashSet<>( - Arrays.asList( - // Grow hierarchy - UnknownGrow.class, - Growable.class, - PolyGrow.class, - // Shrink hierarchy - UnknownShrink.class, - Shrinkable.class, - PolyShrink.class, - // Replace hierarchy - UnknownReplace.class, - Replaceable.class, - PolyReplace.class)); - } - - /** - * Adds a tree annotator that expands alias annotations ({@code @Modifiable}, - * {@code @Unmodifiable}, {@code @UnknownModifiability}, {@code @PolyModifiable}) into their - * constituent qualifiers across all three hierarchies. - */ - @Override - protected TreeAnnotator createTreeAnnotator() { - return new ListTreeAnnotator( - new ModifiabilityAliasAnnotator(this), super.createTreeAnnotator()); - } - - /** - * Expands alias annotations written in source code into their equivalent multi-hierarchy - * qualifiers: - * - *

    - *
  • {@code @Modifiable} → {@code @Growable @Shrinkable @Replaceable} - *
  • {@code @Unmodifiable} or {@code @UnknownModifiability} → - * {@code @UnknownGrow @UnknownShrink @UnknownReplace} - *
  • {@code @PolyModifiable} → {@code @PolyGrow @PolyShrink @PolyReplace} - *
- */ - private class ModifiabilityAliasAnnotator extends TreeAnnotator { - public ModifiabilityAliasAnnotator(AnnotatedTypeFactory factory) { - super(factory); - } - - /** - * Expands alias annotations found in the given list onto the given type. - * - * @param rawAnnos raw annotations from the source tree - * @param type the type to modify - */ - private void expandAliases( - List rawAnnos, AnnotatedTypeMirror type) { - for (AnnotationMirror anno : rawAnnos) { - if (AnnotationUtils.areSame(anno, MODIFIABLE)) { - type.replaceAnnotation(GROWABLE); - type.replaceAnnotation(SHRINKABLE); - type.replaceAnnotation(REPLACEABLE); - break; - } else if (AnnotationUtils.areSame(anno, UNMODIFIABLE) - || AnnotationUtils.areSame(anno, UNKNOWN_MODIFIABILITY)) { - type.replaceAnnotation(UNKNOWN_GROW); - type.replaceAnnotation(UNKNOWN_SHRINK); - type.replaceAnnotation(UNKNOWN_REPLACE); - break; - } else if (AnnotationUtils.areSame(anno, POLY_MODIFIABLE)) { - type.replaceAnnotation(POLY_GROW); - type.replaceAnnotation(POLY_SHRINK); - type.replaceAnnotation(POLY_REPLACE); - break; - } - } - } - - @Override - public Void visitAnnotatedType(AnnotatedTypeTree node, AnnotatedTypeMirror type) { - expandAliases(TreeUtils.annotationsFromTree(node), type); - return super.visitAnnotatedType(node, type); - } - - /** - * Handles alias expansion for variable declarations (local variables, fields, and parameters). - * The tree annotator is called with the {@link VariableTree}, not the inner {@link - * AnnotatedTypeTree}, so {@link #visitAnnotatedType} does not fire for these cases. - */ - @Override - public Void visitVariable(VariableTree node, AnnotatedTypeMirror type) { - Tree typeTree = node.getType(); - if (typeTree instanceof AnnotatedTypeTree) { - expandAliases(TreeUtils.annotationsFromTree((AnnotatedTypeTree) typeTree), type); - } - return super.visitVariable(node, type); - } - } - - @Override - protected TypeAnnotator createTypeAnnotator() { - return new ListTypeAnnotator(new ModifiabilityTypeAnnotator(this), super.createTypeAnnotator()); - } - - /** - * Removes capabilities that cannot be supported by structural constraints of the collection type: - * - *
    - *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to - * {@code @UnknownReplace} - *
  • Map.Entry: remove Grow and Shrink capabilities - *
  • Iterator: remove Grow and Replace capabilities - *
- */ - private class ModifiabilityTypeAnnotator extends TypeAnnotator { - public ModifiabilityTypeAnnotator(ModifiabilityAnnotatedTypeFactory factory) { - super(factory); - } - - @Override - public Void visitDeclared(AnnotatedDeclaredType type, Void p) { - super.visitDeclared(type, p); - - // Skip structural refinement for polymorphic types. - if (type.hasPrimaryAnnotation(POLY_GROW) - || type.hasPrimaryAnnotation(POLY_SHRINK) - || type.hasPrimaryAnnotation(POLY_REPLACE)) { - return null; - } - - TypeMirror underlyingType = type.getUnderlyingType(); - Types types = getProcessingEnv().getTypeUtils(); - TypeMirror erasure = types.erasure(underlyingType); - - if (types.isSubtype(erasure, setErasure) - || (types.isSubtype(erasure, queueErasure) - && !types.isSubtype(erasure, linkedListErasure))) { - // Set or Queue (but not LinkedList): no replace methods → remove Replace capability. - removeReplaceable(type); - } else if (types.isSubtype(erasure, mapEntryErasure)) { - // Map.Entry: no grow or shrink methods → remove both capabilities. - removeGrowable(type); - removeShrinkable(type); - } else if (types.isSubtype(erasure, iteratorErasure)) { - // Iterator: no grow or replace methods → remove both capabilities. - removeGrowable(type); - removeReplaceable(type); - } - - return null; - } - } - - /** - * Removes the Grow capability by setting the Grow hierarchy qualifier to {@code @UnknownGrow}. - */ - private void removeGrowable(AnnotatedTypeMirror type) { - type.replaceAnnotation(UNKNOWN_GROW); - } - - /** - * Removes the Shrink capability by setting the Shrink hierarchy qualifier to - * {@code @UnknownShrink}. - */ - private void removeShrinkable(AnnotatedTypeMirror type) { - type.replaceAnnotation(UNKNOWN_SHRINK); - } - - /** - * Removes the Replace capability by setting the Replace hierarchy qualifier to - * {@code @UnknownReplace}. - */ - private void removeReplaceable(AnnotatedTypeMirror type) { - type.replaceAnnotation(UNKNOWN_REPLACE); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 7cdeeda1f86f..5b79cc22a121 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -1,34 +1,35 @@ package org.checkerframework.checker.modifiability; import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.Map; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.qual.RelevantJavaTypes; -import org.checkerframework.framework.qual.StubFiles; -import org.checkerframework.framework.source.SuppressWarningsPrefix; +import java.util.List; +import org.checkerframework.checker.modifiability.grow.GrowChecker; +import org.checkerframework.checker.modifiability.replace.ReplaceChecker; +import org.checkerframework.checker.modifiability.shrink.ShrinkChecker; +import org.checkerframework.framework.source.AggregateChecker; +import org.checkerframework.framework.source.SourceChecker; /** * A type-checker that warns, at compile time, if a program might throw {@link * UnsupportedOperationException} at run time due to calling a mutating method on a collection. * - *

The checker enforces the Modifiability type system, where {@code @Modifiable} collections can - * be safely mutated and {@code @Unmodifiable} collections cannot be mutated without risking an - * {@link UnsupportedOperationException}. + *

This is an aggregate checker that runs three independent sub-checkers, one for each + * modifiability capability: + * + *

    + *
  • {@link GrowChecker} — checks grow operations (e.g., {@code add}, {@code addAll}) + *
  • {@link ShrinkChecker} — checks shrink operations (e.g., {@code remove}, {@code clear}) + *
  • {@link ReplaceChecker} — checks replace operations (e.g., {@code set}, {@code replaceAll}) + *
* * @checker_framework.manual #modifiability-checker Modifiability Checker */ -@RelevantJavaTypes({ - Collection.class, - Iterator.class, - Map.class, - Map.Entry.class, - Collections.class -}) -@StubFiles({"ical4j.astub"}) -@SuppressWarningsPrefix({"modifiable", "unmodifiable"}) -public class ModifiabilityChecker extends BaseTypeChecker { +public class ModifiabilityChecker extends AggregateChecker { + /** Creates a Modifiability checker. */ public ModifiabilityChecker() {} + + @Override + protected Collection> getSupportedCheckers() { + return List.of(GrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java new file mode 100644 index 000000000000..056a4b58100a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -0,0 +1,115 @@ +package org.checkerframework.checker.modifiability.grow; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyGrow; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.UnknownGrow; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; + +/** The type factory for the Modifiability Checker. */ +public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The erased {@code java.util.Map.Entry} type. */ + private final TypeMirror mapEntryErasure; + + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; + + // ── Hierarchy qualifiers ────────── + + /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ + private AnnotationMirror UNKNOWN_GROW; + + /** The {@code @}{@link Growable} qualifier (bottom of Grow hierarchy). */ + private AnnotationMirror GROWABLE; + + /** The {@code @}{@link PolyGrow} qualifier. */ + private AnnotationMirror POLY_GROW; + + @SuppressWarnings("this-escape") + public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + // Cache type erasures. + Types types = getProcessingEnv().getTypeUtils(); + this.mapEntryErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); + this.iteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + + // Initialize annotation mirrors after the hierarchy is established. + this.UNKNOWN_GROW = AnnotationBuilder.fromClass(getElementUtils(), UnknownGrow.class); + this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); + this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); + + addAliasedTypeAnnotation(Modifiable.class, GROWABLE); + addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_GROW); + addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_GROW); + addAliasedTypeAnnotation(PolyModifiable.class, POLY_GROW); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>(Arrays.asList(UnknownGrow.class, Growable.class, PolyGrow.class)); + } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator(new GrowTypeAnnotator(this), super.createTypeAnnotator()); + } + + /** + * Removes capabilities that cannot be supported by structural constraints of the collection type: + * + *
    + *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to + * {@code @UnknownReplace} + *
  • Map.Entry: remove Grow and Shrink capabilities + *
  • Iterator: remove Grow and Replace capabilities + *
+ */ + private class GrowTypeAnnotator extends TypeAnnotator { + public GrowTypeAnnotator(GrowAnnotatedTypeFactory factory) { + super(factory); + } + + @Override + public Void visitDeclared(AnnotatedDeclaredType type, Void p) { + super.visitDeclared(type, p); + + // Skip structural refinement for polymorphic types. + if (type.hasPrimaryAnnotation(POLY_GROW)) { + return null; + } + + TypeMirror underlyingType = type.getUnderlyingType(); + Types types = getProcessingEnv().getTypeUtils(); + TypeMirror erasure = types.erasure(underlyingType); + + if (types.isSubtype(erasure, mapEntryErasure)) { + // Map.Entry: no grow. + type.replaceAnnotation(UNKNOWN_GROW); + } else if (types.isSubtype(erasure, iteratorErasure)) { + // Iterator: no grow. + type.replaceAnnotation(UNKNOWN_GROW); + } + + return null; + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java new file mode 100644 index 000000000000..71c21d708502 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.modifiability.grow; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * A type-checker that warns, at compile time, if a program might throw {@link + * UnsupportedOperationException} at run time due to calling a grow method on a collection. + * + *

The checker enforces the Modifiability type system, where {@code @Growable} collections can be + * safely mutated and {@code @UnknownGrow} collections cannot call add without risking an {@link + * UnsupportedOperationException}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({ + Collection.class, + Iterator.class, + Map.class, + Map.Entry.class, + Collections.class +}) +@StubFiles({"../ical4j.astub"}) +@SuppressWarningsPrefix({"Growable", "UnknownGrow"}) +public class GrowChecker extends BaseTypeChecker { + /** Creates a Grow checker. */ + public GrowChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java similarity index 85% rename from checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java rename to checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java index 9e1bd3c2566f..c9eb9ddee52f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.modifiability; +package org.checkerframework.checker.modifiability.grow; import com.sun.source.tree.MethodInvocationTree; import javax.lang.model.element.ExecutableElement; @@ -9,12 +9,12 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.TreeUtils; -/** Visitor for the {@link ModifiabilityChecker}. */ -public class ModifiabilityVisitor extends BaseTypeVisitor { +/** Visitor for the {@link GrowChecker}. */ +public class GrowVisitor extends BaseTypeVisitor { - /** Create a visitor for the Modifiability Checker. */ - // MDE: Why is the type of `checker` BaseTypeChecker rather than ModifiabilityChecker? - public ModifiabilityVisitor(BaseTypeChecker checker) { + /** Create a visitor for the Grow Checker. */ + // MDE: Why is the type of `checker` BaseTypeChecker rather than GrowChecker? + public GrowVisitor(BaseTypeChecker checker) { super(checker); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties new file mode 100644 index 000000000000..7eda6bdc0bf3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties @@ -0,0 +1 @@ +usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java new file mode 100644 index 000000000000..184c808d61f2 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -0,0 +1,125 @@ +package org.checkerframework.checker.modifiability.replace; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolyReplace; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.UnknownReplace; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; + +/** The type factory for the Modifiability Checker. */ +public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The erased {@code java.util.Set} type. */ + private final TypeMirror setErasure; + + /** The erased {@code java.util.Queue} type. */ + private final TypeMirror queueErasure; + + /** The erased {@code java.util.LinkedList} type. */ + private final TypeMirror linkedListErasure; + + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; + + // ── Hierarchy qualifiers ────────── + + /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ + private AnnotationMirror UNKNOWN_REPLACE; + + /** The {@code @}{@link Replaceable} qualifier (bottom of Replace hierarchy). */ + private AnnotationMirror REPLACEABLE; + + /** The {@code @}{@link PolyReplace} qualifier. */ + private AnnotationMirror POLY_REPLACE; + + @SuppressWarnings("this-escape") + public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + // Cache type erasures. + Types types = getProcessingEnv().getTypeUtils(); + this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); + this.queueErasure = types.erasure(getElementUtils().getTypeElement("java.util.Queue").asType()); + this.linkedListErasure = + types.erasure(getElementUtils().getTypeElement("java.util.LinkedList").asType()); + this.iteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + + // Initialize annotation mirrors after the hierarchy is established. + this.UNKNOWN_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), UnknownReplace.class); + this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); + this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); + + addAliasedTypeAnnotation(Modifiable.class, REPLACEABLE); + addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_REPLACE); + addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_REPLACE); + addAliasedTypeAnnotation(PolyModifiable.class, POLY_REPLACE); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList(UnknownReplace.class, Replaceable.class, PolyReplace.class)); + } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator(new ReplaceTypeAnnotator(this), super.createTypeAnnotator()); + } + + /** + * Removes capabilities that cannot be supported by structural constraints of the collection type: + * + *

    + *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to + * {@code @UnknownReplace} + *
  • Iterator: remove Replace capabilities + *
+ */ + private class ReplaceTypeAnnotator extends TypeAnnotator { + public ReplaceTypeAnnotator(ReplaceAnnotatedTypeFactory factory) { + super(factory); + } + + @Override + public Void visitDeclared(AnnotatedDeclaredType type, Void p) { + super.visitDeclared(type, p); + + // Skip structural refinement for polymorphic types. + if (type.hasPrimaryAnnotation(POLY_REPLACE)) { + return null; + } + + TypeMirror underlyingType = type.getUnderlyingType(); + Types types = getProcessingEnv().getTypeUtils(); + TypeMirror erasure = types.erasure(underlyingType); + + if (types.isSubtype(erasure, setErasure) + || (types.isSubtype(erasure, queueErasure) + && !types.isSubtype(erasure, linkedListErasure))) { + // Set or Queue (but not LinkedList): Drop R bit + type.replaceAnnotation(UNKNOWN_REPLACE); + } else if (types.isSubtype(erasure, iteratorErasure)) { + // Iterator: Drop G and R bits + type.replaceAnnotation(UNKNOWN_REPLACE); + } + + return null; + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java new file mode 100644 index 000000000000..c4c81fe4357f --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.modifiability.replace; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * A type-checker that warns, at compile time, if a program might throw {@link + * UnsupportedOperationException} at run time due to calling a replace method on a collection. + * + *

The checker enforces the Modifiability type system, where {@code @Replaceable} collections can + * be safely mutated and {@code @UnknownReplace} collections cannot call remove without risking an + * {@link UnsupportedOperationException}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({ + Collection.class, + Iterator.class, + Map.class, + Map.Entry.class, + Collections.class +}) +@StubFiles({"../ical4j.astub"}) +@SuppressWarningsPrefix({"Replaceable", "UnknownReplace"}) +public class ReplaceChecker extends BaseTypeChecker { + /** Creates a Replace checker. */ + public ReplaceChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java new file mode 100644 index 000000000000..40c750dbbcf9 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -0,0 +1,45 @@ +package org.checkerframework.checker.modifiability.replace; + +import com.sun.source.tree.MethodInvocationTree; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.qual.ThrowsUOE; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.TreeUtils; + +/** Visitor for the {@link ReplaceChecker}. */ +public class ReplaceVisitor extends BaseTypeVisitor { + + /** Create a visitor for the Grow Checker. */ + // MDE: Why is the type of `checker` BaseTypeChecker rather than GrowChecker? + public ReplaceVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + ExecutableElement method = TreeUtils.elementFromUse(node); + // Check if the method being invoked is annotated with @ThrowsUOE. + // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw + // UnsupportedOperationException at run time, so we report an error immediately. + if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { + checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); + } + return super.visitMethodInvocation(node, p); + } + + // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they + // exist.) + // Suppress the framework's "constructor result must be TOP" check. + // For Modifiability, constructors may legitimately produce @Modifiable. + // By default, the BaseTypeChecker requires constructors to return the top type + // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) + // produce a @Modifiable object, which is a subtype of @UnknownModifiability. + // We want to allow this so that we can use these objects for mutation without + // explicit casting or annotations. + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties new file mode 100644 index 000000000000..7eda6bdc0bf3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties @@ -0,0 +1 @@ +usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java new file mode 100644 index 000000000000..c072afebdcab --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -0,0 +1,106 @@ +package org.checkerframework.checker.modifiability.shrink; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolyShrink; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnknownModifiability; +import org.checkerframework.checker.modifiability.qual.UnknownShrink; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; + +/** The type factory for the Modifiability Checker. */ +public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The erased {@code java.util.Map.Entry} type. */ + private final TypeMirror mapEntryErasure; + + // ── Hierarchy qualifiers ────────── + + /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ + private AnnotationMirror UNKNOWN_SHRINK; + + /** The {@code @}{@link Shrinkable} qualifier (bottom of Shrink hierarchy). */ + private AnnotationMirror SHRINKABLE; + + /** The {@code @}{@link PolyShrink} qualifier. */ + private AnnotationMirror POLY_SHRINK; + + @SuppressWarnings("this-escape") + public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + // Cache type erasures. + Types types = getProcessingEnv().getTypeUtils(); + this.mapEntryErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); + + // Initialize annotation mirrors after the hierarchy is established. + this.UNKNOWN_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), UnknownShrink.class); + this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); + this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); + + addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); + addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_SHRINK); + addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_SHRINK); + addAliasedTypeAnnotation(PolyModifiable.class, POLY_SHRINK); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList(UnknownShrink.class, Shrinkable.class, PolyShrink.class)); + } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator(new ShrinkTypeAnnotator(this), super.createTypeAnnotator()); + } + + /** + * Removes capabilities that cannot be supported by structural constraints of the collection type: + * + *

    + *
  • Map.Entry: remove Shrink capabilities + *
  • Iterator: remove Shrink capabilities + *
+ */ + private class ShrinkTypeAnnotator extends TypeAnnotator { + public ShrinkTypeAnnotator(ShrinkAnnotatedTypeFactory factory) { + super(factory); + } + + @Override + public Void visitDeclared(AnnotatedDeclaredType type, Void p) { + super.visitDeclared(type, p); + + // Skip structural refinement for polymorphic types. + if (type.hasPrimaryAnnotation(POLY_SHRINK)) { + return null; + } + + TypeMirror underlyingType = type.getUnderlyingType(); + Types types = getProcessingEnv().getTypeUtils(); + TypeMirror erasure = types.erasure(underlyingType); + + if (types.isSubtype(erasure, mapEntryErasure)) { + // Map.Entry: Drop G and S bits + type.replaceAnnotation(UNKNOWN_SHRINK); + } + + return null; + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java new file mode 100644 index 000000000000..99a270bc703c --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.modifiability.shrink; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * A type-checker that warns, at compile time, if a program might throw {@link + * UnsupportedOperationException} at run time due to calling a shrink method on a collection. + * + *

The checker enforces the Modifiability type system, where {@code @Shrinkable} collections can + * be safely mutated and {@code @UnknownShrink} collections cannot call remove without risking an + * {@link UnsupportedOperationException}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({ + Collection.class, + Iterator.class, + Map.class, + Map.Entry.class, + Collections.class +}) +@StubFiles({"../ical4j.astub"}) +@SuppressWarningsPrefix({"Shrinkable", "UnknownShrink"}) +public class ShrinkChecker extends BaseTypeChecker { + /** Creates a Shrink checker. */ + public ShrinkChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java new file mode 100644 index 000000000000..f1463c4db5f0 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -0,0 +1,45 @@ +package org.checkerframework.checker.modifiability.shrink; + +import com.sun.source.tree.MethodInvocationTree; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.qual.ThrowsUOE; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.TreeUtils; + +/** Visitor for the {@link ShrinkChecker}. */ +public class ShrinkVisitor extends BaseTypeVisitor { + + /** Create a visitor for the Shrink Checker. */ + // MDE: Why is the type of `checker` BaseTypeChecker rather than ShrinkChecker? + public ShrinkVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + ExecutableElement method = TreeUtils.elementFromUse(node); + // Check if the method being invoked is annotated with @ThrowsUOE. + // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw + // UnsupportedOperationException at run time, so we report an error immediately. + if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { + checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); + } + return super.visitMethodInvocation(node, p); + } + + // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they + // exist.) + // Suppress the framework's "constructor result must be TOP" check. + // For Modifiability, constructors may legitimately produce @Modifiable. + // By default, the BaseTypeChecker requires constructors to return the top type + // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) + // produce a @Modifiable object, which is a subtype of @UnknownModifiability. + // We want to allow this so that we can use these objects for mutation without + // explicit casting or annotations. + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties new file mode 100644 index 000000000000..7eda6bdc0bf3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties @@ -0,0 +1 @@ +usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 6f26f1dc12fb..bebaba5e3b5d 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -36,16 +36,18 @@ void testSetReplaceRemoved( // @Replaceable Set → Grow=Unknown, Shrink=Unknown, Replace=Unknown (R removed) // @Growable @Replaceable Set → Grow=Growable, Shrink=Unknown, Replace=Unknown (R removed) @Growable Set g1 = growReplace; // OK: Grow still intact - // :: error: (assignment) - @Replaceable Set r1 = growReplace; // Error: R removed → UnknownReplace !<: Replaceable + @Replaceable + Set r1 = + growReplace; // OK: @replaceable set becomes @UnknownMod, GrowReplace set becomes @Growable. // @Growable @Shrinkable @Replaceable Set → Grow=G, Shrink=S, Replace=Unknown (R removed) // effectively the same as @Growable @Shrinkable Set @Growable @Shrinkable Set gs1 = modifiable; // OK - // :: error: (assignment) - @Replaceable Set r2 = modifiable; // Error: R removed + @Replaceable + Set r2 = + modifiable; // OK: replaceale becomes unknown, modifiable becomes @Growable @Shrinkable } void testSetGrowShrinkPreserved( From 1d835d21b9849de5d939ec652bdf3860c41c0293 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 19:02:35 -0800 Subject: [PATCH 047/308] Use an absolute path --- docs/manual/introduction.tex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 4ff2c9ca36e9..494278423a14 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -695,9 +695,10 @@ You must also supply \<-Awarns>, or the inference output may be incomplete. \item \<-AinferOutputDirectory> The directory into which to write inference output (e.g., \<.ajava> - files). It defaults to \, but when using - a build system (e.g., Gradle), the default may be relative to a build - system directory rather than the project root directory. + files). It defaults to \. When using + a build system (e.g., Gradle), the default may be interpreted relative to + a build system directory (e.g., \<$HOME/.gradle/workers>) rather than the + project root directory, so you may need to provide an absolute path. \item \<-AinferOutputOriginal> When outputting \<.ajava> files when running with \<-Ainfer=ajava>, also output a copy of the original file with no inferred annotations, From fd9793bcc89490325d96ae2fe51ff23125289875 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 19:58:44 -0800 Subject: [PATCH 048/308] New `wpi2.sh` script --- checker/bin/wpi2.sh | 30 +++++++++++++++++++ docs/CHANGELOG.md | 2 ++ docs/manual/inference.tex | 61 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100755 checker/bin/wpi2.sh diff --git a/checker/bin/wpi2.sh b/checker/bin/wpi2.sh new file mode 100755 index 000000000000..fcd23d4b7525 --- /dev/null +++ b/checker/bin/wpi2.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# This script performs whole-program inference on a project. +# Run it from the project root. +# Its output is a directory `whole-program-inference-output/` +# that contains .ajava files. + +# For usage, see the "Whole-program inference" +# section of the Checker Framework manual: +# https://checkerframework.org/manual/#whole-program-inference + + +# Exit the script if any statement fails. +set -e + +rm -rf build/whole-program-inference +mkdir -p build/whole-program-inference +rm -rf whole-program-inference-output +mkdir -p whole-program-inference-output +touch whole-program-inference-output/first-iteration.txt + +# The `wpi.diff` file is not used; it is produced for diagnostic purposes. +while ! diff -ur whole-program-inference-output build/whole-program-inference > wpi.diff ; do + rm -rf whole-program-inference-output + mv build/whole-program-inference/ whole-program-inference-output + mv -f wpi.diff wpi-prev.diff + "$@" +done + +rm -rf wpi.diff wpi-prev.diff diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2f535e8de27f..7446b5d79f61 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,8 @@ ### User-visible changes +New `wpi2.sh` script does whole-program inference, but requires buildfile edits. + Removed long-deprecated `-Alint=forbidnonnullarraycomponents`. ### Implementation details diff --git a/docs/manual/inference.tex b/docs/manual/inference.tex index 784cd0b591f5..5613ce1d23a8 100644 --- a/docs/manual/inference.tex +++ b/docs/manual/inference.tex @@ -105,14 +105,20 @@ least upper bound of the types of all the expressions that are assigned into the field. -There are three scripts that you can use to run whole-program inference. +There are four scripts that you can use to run whole-program inference. Each has advantages and disadvantages, discussed below: \begin{itemize} + \item + To run whole-program inference on a single project without modifying its + source code, use the \ script (Section~\ref{wpi-two}). This script + requires you to make a few changes to your buildfile. + \item To run whole-program inference on a single project without modifying its source code, use the \ script (Section~\ref{wpi-one}). This script can automatically understand many Ant, Maven, and Gradle build files, so it requires little manual configuration. + However, it is based on a no-longer-maintained project, \, so it is not guaranteed to work. \item To run whole-program inference on many projects without modifying their source code @@ -130,7 +136,58 @@ that applies to all of them. -\sectionAndLabel{Running whole-program inference on a single project}{wpi-one} +\sectionAndLabel{Running whole-program inference on a single project, with buildfile editing}{wpi-two} + +You need to edit your buildfile, then run the \ script. + + +\subsectionAndLabel{Editing your buildfile}{wpi-two-buildfile} + +First, set up your buildfile to run the Checker Framework. + +Then, add the +\<-Aajava>, +\<-Ainfer>, +\<-AinferOutputDirectory>, and +\<-Awarns> command-line arguments. + +Also remove or comment out \<-Werror>, because the +\ script will halt if the Checker Framework fails, including when it +issues an error. + +Here is a Gradle example: + +\begin{Verbatim} +checkerFramework { + extraJavacArgs = [ + "-Ainfer=ajava", + "-AinferOutputOriginal", // optional + "-AinferOutputDirectory=" + project.projectDir + "/build/whole-program-inference", + "-Aajava=" + project.projectDir + "/whole-program-inference-output", + "-Awarns", + // "-Werror", // remove or comment out any occurrence of "-Werror". + ... + ] + ... +} +\end{Verbatim} + + +\subsectionAndLabel{Running \}{wpi-two-command} + +Second, run \. Its argument is the command that runs the Checker Framework. +Here is a typical invocation: + +\begin{Verbatim} + $CHECKERFRAMEWORK/checker/bin/wpi2.sh ./gradlew clean assemble +\end{Verbatim} + +The output of \ is a top-level directory +\ that contains \<.ajava> files for each file of +your project. + + +\sectionAndLabel{Running whole-program inference on a single project, without buildfile editing}{wpi-one} A typical invocation of \ is From 543f155acfa99ecc810d0c8cd42496af5cfb139c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 20:05:06 -0800 Subject: [PATCH 049/308] Need `clean` target --- docs/manual/inference.tex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/manual/inference.tex b/docs/manual/inference.tex index 5613ce1d23a8..28311f1992fe 100644 --- a/docs/manual/inference.tex +++ b/docs/manual/inference.tex @@ -182,6 +182,10 @@ $CHECKERFRAMEWORK/checker/bin/wpi2.sh ./gradlew clean assemble \end{Verbatim} +The \ task is needed because \ runs the Checker Framework +repeatedly, and Gradle will skip compilation if do nothing if the old (but still +up-to-date) compilation output is present. + The output of \ is a top-level directory \ that contains \<.ajava> files for each file of your project. From dc4fe9f387a237c4dc8b18825676b9b8451d6947 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 20:30:25 -0800 Subject: [PATCH 050/308] Doc --- .../WholeProgramInferenceJavaParserStorage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index a3622deb1617..0426767a6f4f 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -140,7 +140,7 @@ public class WholeProgramInferenceJavaParserStorage /** Maps from binary class name to the source file that contains it. */ private Map classToSource = new HashMap<>(); - /** The directory for reading and writing .ajava files. */ + /** The directory for writing inference output ({@code .ajava} files). */ private final Path inferOutputDirectory; /** True if the {@code -AinferOutputOriginal} option was supplied to the checker. */ @@ -178,7 +178,8 @@ public static boolean isInvisible(Class qual) { * * @param atypeFactory the associated type factory * @param inferOutputDirectory the directory into which to write whole program inference files - * @param inferOutputOriginal true if the -AinferOutputOriginal option was supplied to the checker + * @param inferOutputOriginal true if the {@code -AinferOutputOriginal} option was supplied to the + * checker */ public WholeProgramInferenceJavaParserStorage( AnnotatedTypeFactory atypeFactory, String inferOutputDirectory, boolean inferOutputOriginal) { From 4e14272cc04c3796ad51ffbb3151905a20f077b4 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 20:30:55 -0800 Subject: [PATCH 051/308] Documentation --- .../WholeProgramInferenceJavaParserStorage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index a3622deb1617..0426767a6f4f 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -140,7 +140,7 @@ public class WholeProgramInferenceJavaParserStorage /** Maps from binary class name to the source file that contains it. */ private Map classToSource = new HashMap<>(); - /** The directory for reading and writing .ajava files. */ + /** The directory for writing inference output ({@code .ajava} files). */ private final Path inferOutputDirectory; /** True if the {@code -AinferOutputOriginal} option was supplied to the checker. */ @@ -178,7 +178,8 @@ public static boolean isInvisible(Class qual) { * * @param atypeFactory the associated type factory * @param inferOutputDirectory the directory into which to write whole program inference files - * @param inferOutputOriginal true if the -AinferOutputOriginal option was supplied to the checker + * @param inferOutputOriginal true if the {@code -AinferOutputOriginal} option was supplied to the + * checker */ public WholeProgramInferenceJavaParserStorage( AnnotatedTypeFactory atypeFactory, String inferOutputDirectory, boolean inferOutputOriginal) { From addcb508639ad4f1fde1df9aa61677985e4b3676 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 21:20:31 -0800 Subject: [PATCH 052/308] Quoting --- docs/manual/introduction.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 494278423a14..a0ecbfcb6b01 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -697,7 +697,7 @@ The directory into which to write inference output (e.g., \<.ajava> files). It defaults to \. When using a build system (e.g., Gradle), the default may be interpreted relative to - a build system directory (e.g., \<$HOME/.gradle/workers>) rather than the + a build system directory (e.g., \<\$HOME/.gradle/workers>) rather than the project root directory, so you may need to provide an absolute path. \item \<-AinferOutputOriginal> When outputting \<.ajava> files when running with \<-Ainfer=ajava>, From 89815eb691eba626462f70aad060733a7230a6cc Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 21:24:03 -0800 Subject: [PATCH 053/308] Throw `UserError` on bad input --- .../WholeProgramInferenceJavaParserStorage.java | 7 ++++++- .../WholeProgramInferenceScenesStorage.java | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index 0426767a6f4f..96221f60b4f3 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -45,6 +45,7 @@ import java.lang.annotation.Annotation; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -185,7 +186,11 @@ public WholeProgramInferenceJavaParserStorage( AnnotatedTypeFactory atypeFactory, String inferOutputDirectory, boolean inferOutputOriginal) { this.atypeFactory = atypeFactory; this.elements = atypeFactory.getElementUtils(); - this.inferOutputDirectory = Path.of(inferOutputDirectory); + try { + this.inferOutputDirectory = Path.of(inferOutputDirectory); + } catch (InvalidPathException e) { + throw new UserError("Invalid -AinferOutputDirectory path: " + inferOutputDirectory, e); + } this.inferOutputOriginal = inferOutputOriginal; } diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java index 74f915917782..bdd8cb800b7b 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.IOException; import java.lang.annotation.Target; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; @@ -143,7 +144,11 @@ public WholeProgramInferenceScenesStorage( boolean isNullness = atypeFactory.getClass().getSimpleName().equals("NullnessAnnotatedTypeFactory"); this.ignoreNullAssignments = !isNullness; - this.inferOutputDirectory = Path.of(inferOutputDirectory); + try { + this.inferOutputDirectory = Path.of(inferOutputDirectory); + } catch (InvalidPathException e) { + throw new UserError("Invalid -AinferOutputDirectory path: " + inferOutputDirectory, e); + } } @Override From 7e5b0e330b1abb8fb703d92554323a5ee657a128 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 21:28:00 -0800 Subject: [PATCH 054/308] Shell style --- checker/bin/wpi2.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/checker/bin/wpi2.sh b/checker/bin/wpi2.sh index fcd23d4b7525..24f5cd2f6837 100755 --- a/checker/bin/wpi2.sh +++ b/checker/bin/wpi2.sh @@ -9,7 +9,6 @@ # section of the Checker Framework manual: # https://checkerframework.org/manual/#whole-program-inference - # Exit the script if any statement fails. set -e @@ -20,11 +19,11 @@ mkdir -p whole-program-inference-output touch whole-program-inference-output/first-iteration.txt # The `wpi.diff` file is not used; it is produced for diagnostic purposes. -while ! diff -ur whole-program-inference-output build/whole-program-inference > wpi.diff ; do +while ! diff -ur whole-program-inference-output build/whole-program-inference > wpi.diff; do rm -rf whole-program-inference-output mv build/whole-program-inference/ whole-program-inference-output mv -f wpi.diff wpi-prev.diff "$@" done -rm -rf wpi.diff wpi-prev.diff +rm -rf wpi.diff wpi-prev.diff From 88ce7621fc29b773bcdabd961919435c4e108ac9 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 21 Feb 2026 21:30:43 -0800 Subject: [PATCH 055/308] Throw `UserError` on bad input --- .../WholeProgramInferenceScenesStorage.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java index bdd8cb800b7b..936cd22cc531 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenesStorage.java @@ -483,9 +483,10 @@ public boolean addClassDeclarationAnnotation(TypeElement classElt, AnnotationMir */ public void writeScenes(OutputFormat outputFormat, BaseTypeChecker checker) { // Create WPI directory if it doesn't exist already. - File jaifDir = inferOutputDirectory.toFile(); - if (!jaifDir.exists()) { - jaifDir.mkdirs(); + try { + java.nio.file.Files.createDirectories(inferOutputDirectory); + } catch (java.io.IOException e) { + throw new UserError("Cannot create " + inferOutputDirectory.toAbsolutePath(), e); } // Write scenes into files. for (String jaifPath : modifiedScenes) { From 677b5a551361e55f4c2a2e1e1dcd46c0ca30e7f6 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 22 Feb 2026 13:20:13 -0800 Subject: [PATCH 056/308] added a modifiability visitor so that all three visitors extend modifiability visitor --- .../modifiability/ModifiabilityVisitor.java | 58 +++++++++++++++++++ .../modifiability/grow/GrowVisitor.java | 36 +----------- .../modifiability/replace/ReplaceVisitor.java | 38 +----------- .../modifiability/shrink/ShrinkVisitor.java | 36 +----------- 4 files changed, 65 insertions(+), 103 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java new file mode 100644 index 000000000000..daafc9c24ef5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -0,0 +1,58 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.MethodInvocationTree; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.qual.ThrowsUOE; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.TreeUtils; + +/** + * Base visitor for the Modifiability sub-checkers (Grow, Shrink, Replace). + * + *

This class contains logic shared across all three sub-checkers: + * + *

    + *
  • Reporting errors for invocations of methods annotated with {@link ThrowsUOE}. + *
  • Suppressing the "constructor result must be TOP" check, since collection constructors may + * legitimately produce {@code @Modifiable}. + *
+ */ +public class ModifiabilityVisitor extends BaseTypeVisitor { + + /** + * Create a ModifiabilityVisitor. + * + * @param checker the checker that uses this visitor + */ + public ModifiabilityVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + ExecutableElement method = TreeUtils.elementFromUse(node); + // Check if the method being invoked is annotated with @ThrowsUOE. + // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw + // UnsupportedOperationException at run time, so we report an error immediately. + if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { + checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); + } + return super.visitMethodInvocation(node, p); + } + + // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they + // exist.) + // Suppress the framework's "constructor result must be TOP" check. + // For Modifiability, constructors may legitimately produce @Modifiable. + // By default, the BaseTypeChecker requires constructors to return the top type + // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) + // produce a @Modifiable object, which is a subtype of @UnknownModifiability. + // We want to allow this so that we can use these objects for mutation without + // explicit casting or annotations. + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java index c9eb9ddee52f..d5b068d343c0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java @@ -1,45 +1,13 @@ package org.checkerframework.checker.modifiability.grow; -import com.sun.source.tree.MethodInvocationTree; -import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.modifiability.qual.ThrowsUOE; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.TreeUtils; /** Visitor for the {@link GrowChecker}. */ -public class GrowVisitor extends BaseTypeVisitor { +public class GrowVisitor extends ModifiabilityVisitor { /** Create a visitor for the Grow Checker. */ - // MDE: Why is the type of `checker` BaseTypeChecker rather than GrowChecker? public GrowVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - ExecutableElement method = TreeUtils.elementFromUse(node); - // Check if the method being invoked is annotated with @ThrowsUOE. - // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw - // UnsupportedOperationException at run time, so we report an error immediately. - if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { - checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); - } - return super.visitMethodInvocation(node, p); - } - - // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they - // exist.) - // Suppress the framework's "constructor result must be TOP" check. - // For Modifiability, constructors may legitimately produce @Modifiable. - // By default, the BaseTypeChecker requires constructors to return the top type - // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) - // produce a @Modifiable object, which is a subtype of @UnknownModifiability. - // We want to allow this so that we can use these objects for mutation without - // explicit casting or annotations. - @Override - protected void checkConstructorResult( - AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 40c750dbbcf9..55d17625b0c5 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -1,45 +1,13 @@ package org.checkerframework.checker.modifiability.replace; -import com.sun.source.tree.MethodInvocationTree; -import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.modifiability.qual.ThrowsUOE; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.TreeUtils; /** Visitor for the {@link ReplaceChecker}. */ -public class ReplaceVisitor extends BaseTypeVisitor { +public class ReplaceVisitor extends ModifiabilityVisitor { - /** Create a visitor for the Grow Checker. */ - // MDE: Why is the type of `checker` BaseTypeChecker rather than GrowChecker? + /** Create a visitor for the Replace Checker. */ public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - ExecutableElement method = TreeUtils.elementFromUse(node); - // Check if the method being invoked is annotated with @ThrowsUOE. - // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw - // UnsupportedOperationException at run time, so we report an error immediately. - if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { - checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); - } - return super.visitMethodInvocation(node, p); - } - - // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they - // exist.) - // Suppress the framework's "constructor result must be TOP" check. - // For Modifiability, constructors may legitimately produce @Modifiable. - // By default, the BaseTypeChecker requires constructors to return the top type - // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) - // produce a @Modifiable object, which is a subtype of @UnknownModifiability. - // We want to allow this so that we can use these objects for mutation without - // explicit casting or annotations. - @Override - protected void checkConstructorResult( - AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index f1463c4db5f0..febfe4a9a808 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -1,45 +1,13 @@ package org.checkerframework.checker.modifiability.shrink; -import com.sun.source.tree.MethodInvocationTree; -import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.modifiability.qual.ThrowsUOE; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.TreeUtils; /** Visitor for the {@link ShrinkChecker}. */ -public class ShrinkVisitor extends BaseTypeVisitor { +public class ShrinkVisitor extends ModifiabilityVisitor { /** Create a visitor for the Shrink Checker. */ - // MDE: Why is the type of `checker` BaseTypeChecker rather than ShrinkChecker? public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - ExecutableElement method = TreeUtils.elementFromUse(node); - // Check if the method being invoked is annotated with @ThrowsUOE. - // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw - // UnsupportedOperationException at run time, so we report an error immediately. - if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { - checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); - } - return super.visitMethodInvocation(node, p); - } - - // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they - // exist.) - // Suppress the framework's "constructor result must be TOP" check. - // For Modifiability, constructors may legitimately produce @Modifiable. - // By default, the BaseTypeChecker requires constructors to return the top type - // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) - // produce a @Modifiable object, which is a subtype of @UnknownModifiability. - // We want to allow this so that we can use these objects for mutation without - // explicit casting or annotations. - @Override - protected void checkConstructorResult( - AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} } From dace742e236f7b47c2d9fcbe7d4e5cbde438d930 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 22 Feb 2026 20:34:59 -0800 Subject: [PATCH 057/308] added PolyShrinkTest --- .../tests/modifiability/PolyShrinkTest.java | 127 +++++++++--------- 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java index e58a88c130cc..baca19c651a7 100644 --- a/checker/tests/modifiability/PolyShrinkTest.java +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -1,61 +1,66 @@ -// import java.util.List; -// import java.util.Set; -// import java.util.Map; -// import org.checkerframework.checker.modifiability.qual.*; - -// public class PolyShrinkTest { - -// // @PolyShrink behaves like Map.keySet() -// // It preserves Shrink capability from argument. -// // Grow and Replace should be defaulted to Unknown (absent). -// @PolyShrink Set keySet(@PolyShrink Map map) { -// return null; -// } - -// void testPolyShrink( -// @Modifiable Map mod, -// @GrowShrink Map gs, -// @Growable Map g, -// @Shrinkable Map s, -// @Unmodifiable Map unmod -// ) { -// // Input: Modifiable (G, S, R) -// // Output: Shrinkable (S). -// // G and R stripped. -// @Shrinkable Set s1 = keySet(mod); -// // :: error: (assignment) -// @Growable Set g1 = keySet(mod); // No G -// // :: error: (assignment) -// @Replaceable Set r1 = keySet(mod); // No R -// // :: error: (assignment) -// @Modifiable Set m1 = keySet(mod); // No G/R - -// // Input: GrowShrink (G, S) -// // Output: Shrinkable (S). -// // G stripped. -// @Shrinkable Set s2 = keySet(gs); -// // :: error: (assignment) -// @Growable Set g2 = keySet(gs); // No G - -// // Input: Growable (G) -// // Output: UnknownModifiability (None). -// // G stripped. No S to preserve. -// @UnknownModifiability Set u3 = keySet(g); -// // :: error: (assignment) -// @Shrinkable Set s3 = keySet(g); // No S -// // :: error: (assignment) -// @Growable Set g3 = keySet(g); // No G - -// // Input: Shrinkable (S) -// // Output: Shrinkable (S). -// @Shrinkable Set s4 = keySet(s); -// // :: error: (assignment) -// @Growable Set g4 = keySet(s); - -// // Input: Unmodifiable (None) -// // Output: UnknownModifiability. -// @UnknownModifiability Set u5 = keySet(unmod); -// // :: error: (assignment) -// @Shrinkable Set s5 = keySet(unmod); -// } -// } +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.*; + +public class PolyShrinkTest { + + void testMapKeySet() { + // Modifiable map: keySet() returns @Shrinkable. Cannot add or replace. + @Modifiable Map modMap = new java.util.HashMap<>(); + @Shrinkable Set modKeySet = modMap.keySet(); + // :: error: (assignment) + @Growable Set modKeySetG = modMap.keySet(); // Cannot add + + // Unmodifiable map: keySet() returns @Unmodifiable. Cannot shrink, add, or replace. + @Unmodifiable Map unmodMap = Map.of("a", "b"); + @Unmodifiable Set unmodKeySet = unmodMap.keySet(); + // :: error: (assignment) + @Shrinkable Set unmodKeySetS = unmodMap.keySet(); // Cannot shrink + // :: error: (assignment) + @Growable Set unmodKeySetG = unmodMap.keySet(); // Cannot add + } + + void testMapValues() { + // Modifiable map: values() returns @Shrinkable. Cannot add or replace. + @Modifiable Map modMap = new java.util.HashMap<>(); + @Shrinkable Collection modVals = modMap.values(); + // :: error: (assignment) + @Growable Collection modValsG = modMap.values(); // Cannot add + // :: error: (assignment) + @Replaceable Collection modValsR = modMap.values(); // Cannot replace + + // Unmodifiable map: values() returns @Unmodifiable. Cannot shrink, add, or replace. + @Unmodifiable Map unmodMap = Map.of("a", "b"); + @Unmodifiable Collection unmodVals = unmodMap.values(); + // :: error: (assignment) + @Shrinkable Collection unmodValsS = unmodMap.values(); // Cannot shrink + // :: error: (assignment) + @Growable Collection unmodValsG = unmodMap.values(); // Cannot add + // :: error: (assignment) + @Replaceable Collection unmodValsR = unmodMap.values(); // Cannot replace + } + + void testMapEntrySet() { + // entrySet(): Set is @PolyShrink (cannot add), Entry is @PolyModifiable. + + // Modifiable map: Set is @Shrinkable, Entry is @Modifiable. + @Modifiable Map modMap = new java.util.HashMap<>(); + @Shrinkable Set> modEntries = modMap.entrySet(); + // :: error: (assignment) + @Growable + Set> modEntriesG = modMap.entrySet(); // Set cannot add + + // Unmodifiable map: Set is @Unmodifiable, Entry is @Unmodifiable. + @Unmodifiable Map unmodMap = Map.of("a", "b"); + @Unmodifiable Set> unmodEntries = unmodMap.entrySet(); + // :: error: (assignment) + @Shrinkable + Set> unmodEntriesS = + unmodMap.entrySet(); // Set cannot shrink + // :: error: (assignment) + @Growable + Set> unmodEntriesG = + unmodMap.entrySet(); // Set cannot add + } +} From d0dfecec51b272ca0cf8197ea9452a6732f11996 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 23 Feb 2026 16:59:23 -0800 Subject: [PATCH 058/308] Fix formatting of type annotations --- build.gradle | 13 +++ .../CollectionsModifiableTest.java | 3 +- .../tests/modifiability/HierarchyTest.java | 40 ++------- .../modifiability/PolyModifiableTest.java | 27 ++---- .../tests/modifiability/PolyShrinkTest.java | 9 +- checker/tests/modifiability/SetTest.java | 89 +++++-------------- .../tests/all-systems/GenericsCasts.java | 3 +- .../tests/all-systems/RawTypeAssignment.java | 3 +- 8 files changed, 54 insertions(+), 133 deletions(-) diff --git a/build.gradle b/build.gradle index 37e2e62d98e0..92c5523d889a 100644 --- a/build.gradle +++ b/build.gradle @@ -354,6 +354,19 @@ allprojects { currentProj -> googleJavaFormat(libs.google.java.format.get().version) // the formatter to apply to Java files } formatAnnotations() + .addTypeAnnotation("Growable") + .addTypeAnnotation("Modifiable") + .addTypeAnnotation("PolyGrow") + .addTypeAnnotation("PolyModifiable") + .addTypeAnnotation("PolyReplace") + .addTypeAnnotation("PolyShrink") + .addTypeAnnotation("Replaceable") + .addTypeAnnotation("Shrinkable") + .addTypeAnnotation("UnknownGrow") + .addTypeAnnotation("UnknownModifiability") + .addTypeAnnotation("UnknownReplace") + .addTypeAnnotation("UnknownShrink") + .addTypeAnnotation("Unmodifiable") } // Only define a groovyGradle task on the root project, for simplicity in setting the target pattern diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index 1991d3d4e110..53681b9c9e21 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -58,8 +58,7 @@ void testCheckedWrappers() { checkedL.add("a"); @Modifiable Map m = new HashMap<>(); - @Modifiable - Map checkedM = Collections.checkedMap(m, String.class, String.class); + @Modifiable Map checkedM = Collections.checkedMap(m, String.class, String.class); checkedM.put("a", "b"); @Modifiable Set s = new HashSet<>(); diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index 098a2d139fe1..d7396a84b1d8 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -104,38 +104,14 @@ void testCombinations( @Replaceable Object rv8 = none; // ── Assignments to full @UnknownGrow @UnknownShrink @UnknownReplace (top in all) ── - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv1 = gsr; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv2 = gs; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv3 = gr; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv4 = sr; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv5 = g; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv6 = s; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv7 = r; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Object tv8 = none; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv1 = gsr; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv2 = gs; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv3 = gr; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv4 = sr; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv5 = g; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv6 = s; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv7 = r; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv8 = none; } // ============================================================ diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 96c3f5ef984c..0edc603998b6 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -14,8 +14,7 @@ public class PolyModifiableTest { /** A simple polymorphic identity method that preserves all three capabilities. */ - @PolyModifiable - List identity(@PolyModifiable List x) { + @PolyModifiable List identity(@PolyModifiable List x) { return x; } @@ -39,9 +38,7 @@ void testPoly( // ============================================================ // Identity on @Growable @Shrinkable (G+S, R=UnknownReplace) // ============================================================ - @Growable - @Shrinkable - List gs1 = identity(gs); // OK + @Growable @Shrinkable List gs1 = identity(gs); // OK // :: error: (assignment) @Modifiable List gs2 = identity(gs); // Error: R=Unknown !<: Replaceable @Growable List gs3 = identity(gs); // OK: G+S+UnknownR <: G @@ -52,9 +49,7 @@ void testPoly( // ============================================================ // Identity on @Growable @Replaceable (G+R, S=UnknownShrink) // ============================================================ - @Growable - @Replaceable - List gr1 = identity(gr); // OK + @Growable @Replaceable List gr1 = identity(gr); // OK // :: error: (assignment) @Modifiable List gr2 = identity(gr); // Error: S=Unknown !<: Shrinkable @Growable List gr3 = identity(gr); // OK @@ -65,9 +60,7 @@ void testPoly( // ============================================================ // Identity on @Shrinkable @Replaceable (S+R, G=UnknownGrow) // ============================================================ - @Shrinkable - @Replaceable - List sr1 = identity(sr); // OK + @Shrinkable @Replaceable List sr1 = identity(sr); // OK // :: error: (assignment) @Modifiable List sr2 = identity(sr); // Error: G=Unknown !<: Growable // :: error: (assignment) @@ -80,9 +73,7 @@ void testPoly( // ============================================================ @Growable List g1 = identity(g); // OK // :: error: (assignment) - @Growable - @Shrinkable - List g2 = identity(g); // Error: S=Unknown + @Growable @Shrinkable List g2 = identity(g); // Error: S=Unknown // :: error: (assignment) @Modifiable List g3 = identity(g); // Error: S=Unknown, R=Unknown @@ -91,9 +82,7 @@ void testPoly( // ============================================================ @Shrinkable List s1 = identity(s); // OK // :: error: (assignment) - @Growable - @Shrinkable - List s2 = identity(s); // Error: G=Unknown + @Growable @Shrinkable List s2 = identity(s); // Error: G=Unknown // :: error: (assignment) @Modifiable List s3 = identity(s); // Error @@ -102,9 +91,7 @@ void testPoly( // ============================================================ @Replaceable List r1 = identity(r); // OK // :: error: (assignment) - @Growable - @Replaceable - List r2 = identity(r); // Error: G=Unknown + @Growable @Replaceable List r2 = identity(r); // Error: G=Unknown // :: error: (assignment) @Modifiable List r3 = identity(r); // Error diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java index baca19c651a7..d69347ce3075 100644 --- a/checker/tests/modifiability/PolyShrinkTest.java +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -48,19 +48,16 @@ void testMapEntrySet() { @Modifiable Map modMap = new java.util.HashMap<>(); @Shrinkable Set> modEntries = modMap.entrySet(); // :: error: (assignment) - @Growable - Set> modEntriesG = modMap.entrySet(); // Set cannot add + @Growable Set> modEntriesG = modMap.entrySet(); // Set cannot add // Unmodifiable map: Set is @Unmodifiable, Entry is @Unmodifiable. @Unmodifiable Map unmodMap = Map.of("a", "b"); @Unmodifiable Set> unmodEntries = unmodMap.entrySet(); // :: error: (assignment) - @Shrinkable - Set> unmodEntriesS = + @Shrinkable Set> unmodEntriesS = unmodMap.entrySet(); // Set cannot shrink // :: error: (assignment) - @Growable - Set> unmodEntriesG = + @Growable Set> unmodEntriesG = unmodMap.entrySet(); // Set cannot add } } diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index bebaba5e3b5d..9565cb711cd1 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -36,17 +36,13 @@ void testSetReplaceRemoved( // @Replaceable Set → Grow=Unknown, Shrink=Unknown, Replace=Unknown (R removed) // @Growable @Replaceable Set → Grow=Growable, Shrink=Unknown, Replace=Unknown (R removed) @Growable Set g1 = growReplace; // OK: Grow still intact - @Replaceable - Set r1 = + @Replaceable Set r1 = growReplace; // OK: @replaceable set becomes @UnknownMod, GrowReplace set becomes @Growable. // @Growable @Shrinkable @Replaceable Set → Grow=G, Shrink=S, Replace=Unknown (R removed) // effectively the same as @Growable @Shrinkable Set - @Growable - @Shrinkable - Set gs1 = modifiable; // OK - @Replaceable - Set r2 = + @Growable @Shrinkable Set gs1 = modifiable; // OK + @Replaceable Set r2 = modifiable; // OK: replaceale becomes unknown, modifiable becomes @Growable @Shrinkable } @@ -57,18 +53,9 @@ void testSetGrowShrinkPreserved( @Growable @Shrinkable Set gs) { // Top in all hierarchies: accepts everything - @UnknownGrow - @UnknownShrink - @UnknownReplace - Set u1 = growable; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Set u2 = shrinkable; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Set u3 = gs; + @UnknownGrow @UnknownShrink @UnknownReplace Set u1 = growable; + @UnknownGrow @UnknownShrink @UnknownReplace Set u2 = shrinkable; + @UnknownGrow @UnknownShrink @UnknownReplace Set u3 = gs; // @Growable Set: Grow=G, Shrink=Unknown, Replace=Unknown @Growable Set g1 = gs; // OK: G+S <: G @@ -84,13 +71,8 @@ void testSetGrowShrinkPreserved( void testQueueLikeSet( @Modifiable Queue modifiable, @Growable @Shrinkable Queue gs) { // Queue also has Replace removed (same rule as Set). - @Growable - @Shrinkable - Queue q1 = modifiable; // OK: modifiable <: gs (R removed from both) - @Growable - @Shrinkable - @Replaceable - Queue q2 = gs; // OK: same after TypeAnnotator + @Growable @Shrinkable Queue q1 = modifiable; // OK: modifiable <: gs (R removed from both) + @Growable @Shrinkable @Replaceable Queue q2 = gs; // OK: same after TypeAnnotator } // ========================================================== @@ -157,56 +139,25 @@ void testIteratorAssignment( @Growable @Shrinkable @Replaceable Iterator modifiable) { // Group 1: No Shrink bit → effective @UnknownGrow @UnknownShrink @UnknownReplace - @UnknownGrow - @UnknownShrink - @UnknownReplace - Iterator u1 = growable; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Iterator u2 = replaceable; - @UnknownGrow - @UnknownShrink - @UnknownReplace - Iterator u3 = growReplace; + @UnknownGrow @UnknownShrink @UnknownReplace Iterator u1 = growable; + @UnknownGrow @UnknownShrink @UnknownReplace Iterator u2 = replaceable; + @UnknownGrow @UnknownShrink @UnknownReplace Iterator u3 = growReplace; // They are all effectively the same type after TypeAnnotator: @Growable Iterator g1 = unknown; @Replaceable Iterator r1 = growReplace; - @Growable - @Replaceable - Iterator gr1 = replaceable; + @Growable @Replaceable Iterator gr1 = replaceable; // Group 2: Has Shrink bit → effective @UnknownGrow @Shrinkable @UnknownReplace - @UnknownGrow - @Shrinkable - @UnknownReplace - Iterator s1 = shrinkable; - @UnknownGrow - @Shrinkable - @UnknownReplace - Iterator s2 = growShrink; - @UnknownGrow - @Shrinkable - @UnknownReplace - Iterator s3 = shrinkReplace; - @UnknownGrow - @Shrinkable - @UnknownReplace - Iterator s4 = modifiable; + @UnknownGrow @Shrinkable @UnknownReplace Iterator s1 = shrinkable; + @UnknownGrow @Shrinkable @UnknownReplace Iterator s2 = growShrink; + @UnknownGrow @Shrinkable @UnknownReplace Iterator s3 = shrinkReplace; + @UnknownGrow @Shrinkable @UnknownReplace Iterator s4 = modifiable; // Cross-assignments within group 2: @Shrinkable Iterator sh1 = modifiable; - @Growable - @Shrinkable - Iterator gs1 = modifiable; - @Shrinkable - @Replaceable - Iterator sr1 = modifiable; - @Growable - @Shrinkable - Iterator gs2 = shrinkable; - @Shrinkable - @Replaceable - Iterator sr2 = growShrink; + @Growable @Shrinkable Iterator gs1 = modifiable; + @Shrinkable @Replaceable Iterator sr1 = modifiable; + @Growable @Shrinkable Iterator gs2 = shrinkable; + @Shrinkable @Replaceable Iterator sr2 = growShrink; // :: error: (assignment) @Shrinkable Iterator bad1 = growable; // Error: Shrink=Unknown !<: Shrinkable diff --git a/framework/tests/all-systems/GenericsCasts.java b/framework/tests/all-systems/GenericsCasts.java index fa5adab8f467..524ba4ec2d1c 100644 --- a/framework/tests/all-systems/GenericsCasts.java +++ b/framework/tests/all-systems/GenericsCasts.java @@ -27,8 +27,7 @@ void m() { // are currently problematic. // TODO: try to find a problem with skipping this check. class Test { - @Modifiable - class Entry extends LinkedList {} + @Modifiable class Entry extends LinkedList {} class Queue { List poll() { diff --git a/framework/tests/all-systems/RawTypeAssignment.java b/framework/tests/all-systems/RawTypeAssignment.java index 880943ceb9b9..b42d16dad10f 100644 --- a/framework/tests/all-systems/RawTypeAssignment.java +++ b/framework/tests/all-systems/RawTypeAssignment.java @@ -4,8 +4,7 @@ class Component {} -@Modifiable -class Components extends ArrayList {} +@Modifiable class Components extends ArrayList {} // If we include a type parameter in the superclass, then there is no error below. // class Components extends ArrayList {} From 7456fd932aaa2bb3a48ef740b2ce1c2f666f50bf Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 24 Feb 2026 22:06:53 -0800 Subject: [PATCH 059/308] small edits --- checker/tests/modifiability/PolyShrinkTest.java | 6 ++---- docs/manual/modifiability-checker.tex | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java index d69347ce3075..ffb9d0a81495 100644 --- a/checker/tests/modifiability/PolyShrinkTest.java +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -54,10 +54,8 @@ void testMapEntrySet() { @Unmodifiable Map unmodMap = Map.of("a", "b"); @Unmodifiable Set> unmodEntries = unmodMap.entrySet(); // :: error: (assignment) - @Shrinkable Set> unmodEntriesS = - unmodMap.entrySet(); // Set cannot shrink + @Shrinkable Set> unmodEntriesS = unmodMap.entrySet(); // :: error: (assignment) - @Growable Set> unmodEntriesG = - unmodMap.entrySet(); // Set cannot add + @Growable Set> unmodEntriesG = unmodMap.entrySet(); } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index badfc5e39751..3e7ab4443481 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -197,7 +197,7 @@ Modifiability Checker automatically removes capabilities that the type cannot support. -\paragraph{Set and Queue (except LinkedList).} +\paragraphAndLabel{Set and Queue (except LinkedList).}{modifiability-set-queue} The \code{Set} interface has no replace methods (\code{set} is a \code{List}-only operation). Similarly, \code{Queue} has no replace methods in general. The Modifiability Checker therefore sets the @@ -214,7 +214,7 @@ (\<@UnknownReplace> is already the default) \end{itemize} -\paragraph{Map.Entry.} +\paragraphAndLabel{Map.Entry.}{modifiability-map-entry} \code{Map.Entry} has only the optional \code{setValue} method (a replace operation) and no grow or shrink operations. The Modifiability Checker therefore sets the Grow and Shrink hierarchy qualifiers to @@ -227,7 +227,7 @@ treated as \code{Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry} \end{itemize} -\paragraph{Iterator.} +\paragraphAndLabel{Iterator.}{modifiability-iterator} \code{Iterator} has only the optional \code{remove} method (a shrink operation) and no grow or replace operations. (Note that \code{ListIterator} is treated normally because it also has optional From 3fce1dfd3d892e85c05cfe612e48c14ba9327b63 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 25 Feb 2026 16:16:15 -0800 Subject: [PATCH 060/308] Allow sets to be marked @Modifiable. --- .../replace/ReplaceAnnotatedTypeFactory.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 184c808d61f2..eda5e4e216a7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -17,9 +17,12 @@ import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.QualifierUpperBounds; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.TypesUtils; /** The type factory for the Modifiability Checker. */ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -122,4 +125,22 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { return null; } } + + @Override + protected QualifierUpperBounds createQualifierUpperBounds() { + return new QualifierUpperBounds(this) { + private final AnnotationMirrorSet unknownReplaceSet = + AnnotationMirrorSet.singleton(UNKNOWN_REPLACE); + + @Override + public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { + if (TypesUtils.isErasedSubtype(type, setErasure, types)) { + // Sets elements of a set can never be replaced, so treat them as @UnknownReplace. Even if + // they are annotation @Modifiable in a stubfile. + return unknownReplaceSet; + } + return super.getBoundQualifiers(type); + } + }; + } } From b745db76cbfd4d6aef307a17736916d693d80007 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 25 Feb 2026 16:25:19 -0800 Subject: [PATCH 061/308] Correct comment. --- .../modifiability/replace/ReplaceAnnotatedTypeFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index eda5e4e216a7..82855fb248c5 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -135,7 +135,7 @@ protected QualifierUpperBounds createQualifierUpperBounds() { @Override public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { if (TypesUtils.isErasedSubtype(type, setErasure, types)) { - // Sets elements of a set can never be replaced, so treat them as @UnknownReplace. Even if + // Elements of a set can never be replaced, so treat them as @UnknownReplace. Even if // they are annotation @Modifiable in a stubfile. return unknownReplaceSet; } From 9d89f2daaef8313e11263c7b2f2db24f09daffa3 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 25 Feb 2026 23:23:56 -0800 Subject: [PATCH 062/308] in grow, shrink, and replace, change the upperbound of irrelevant types to be top. --- .../grow/GrowAnnotatedTypeFactory.java | 37 ++++++++++++++++--- .../replace/ReplaceAnnotatedTypeFactory.java | 34 ++++++++++++----- .../shrink/ShrinkAnnotatedTypeFactory.java | 25 +++++++++++-- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 056a4b58100a..c8b3b1b5a64b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -17,9 +17,12 @@ import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.QualifierUpperBounds; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.TypesUtils; /** The type factory for the Modifiability Checker. */ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -30,6 +33,9 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Iterator} type. */ private final TypeMirror iteratorErasure; + /** The erased {@code java.util.ListIterator} type. */ + private final TypeMirror listIteratorErasure; + // ── Hierarchy qualifiers ────────── /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ @@ -50,7 +56,8 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); this.iteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); - + this.listIteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. this.UNKNOWN_GROW = AnnotationBuilder.fromClass(getElementUtils(), UnknownGrow.class); this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); @@ -98,13 +105,12 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { } TypeMirror underlyingType = type.getUnderlyingType(); - Types types = getProcessingEnv().getTypeUtils(); - TypeMirror erasure = types.erasure(underlyingType); - if (types.isSubtype(erasure, mapEntryErasure)) { + if (TypesUtils.isErasedSubtype(underlyingType, mapEntryErasure, types)) { // Map.Entry: no grow. type.replaceAnnotation(UNKNOWN_GROW); - } else if (types.isSubtype(erasure, iteratorErasure)) { + } else if (TypesUtils.isErasedSubtype(underlyingType, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(underlyingType, listIteratorErasure, types)) { // Iterator: no grow. type.replaceAnnotation(UNKNOWN_GROW); } @@ -112,4 +118,25 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { return null; } } + + @Override + protected QualifierUpperBounds createQualifierUpperBounds() { + return new QualifierUpperBounds(this) { + private final AnnotationMirrorSet unknownGrow = AnnotationMirrorSet.singleton(UNKNOWN_GROW); + + @Override + public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { + if (TypesUtils.isErasedSubtype(type, mapEntryErasure, types)) { + // Elements of a map entry can never be grown, so treat them as @UnknownGrow. Even if + // they are annotation @Growable in a stubfile. + return unknownGrow; + } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { + // It's a standard Iterator, but NOT a ListIterator: Drop G bit + return unknownGrow; + } + return super.getBoundQualifiers(type); + } + }; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 82855fb248c5..cde0ab0a3e74 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -39,6 +39,9 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Iterator} type. */ private final TypeMirror iteratorErasure; + /** The erased {@code java.util.ListIterator} type. */ + private final TypeMirror listIteratorErasure; + // ── Hierarchy qualifiers ────────── /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ @@ -61,6 +64,8 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.LinkedList").asType()); this.iteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + this.listIteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. this.UNKNOWN_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), UnknownReplace.class); @@ -109,19 +114,19 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { } TypeMirror underlyingType = type.getUnderlyingType(); - Types types = getProcessingEnv().getTypeUtils(); - TypeMirror erasure = types.erasure(underlyingType); - if (types.isSubtype(erasure, setErasure) - || (types.isSubtype(erasure, queueErasure) - && !types.isSubtype(erasure, linkedListErasure))) { - // Set or Queue (but not LinkedList): Drop R bit + if (TypesUtils.isErasedSubtype(underlyingType, setErasure, types)) { + // Set: Drop R bit + type.replaceAnnotation(UNKNOWN_REPLACE); + } else if (TypesUtils.isErasedSubtype(underlyingType, queueErasure, types) + && !TypesUtils.isErasedSubtype(underlyingType, linkedListErasure, types)) { + // Queue (but not LinkedList): Drop R bit type.replaceAnnotation(UNKNOWN_REPLACE); - } else if (types.isSubtype(erasure, iteratorErasure)) { + } else if (TypesUtils.isErasedSubtype(underlyingType, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(underlyingType, listIteratorErasure, types)) { // Iterator: Drop G and R bits type.replaceAnnotation(UNKNOWN_REPLACE); } - return null; } } @@ -129,7 +134,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { @Override protected QualifierUpperBounds createQualifierUpperBounds() { return new QualifierUpperBounds(this) { - private final AnnotationMirrorSet unknownReplaceSet = + private final AnnotationMirrorSet unknownReplace = AnnotationMirrorSet.singleton(UNKNOWN_REPLACE); @Override @@ -137,7 +142,16 @@ public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { if (TypesUtils.isErasedSubtype(type, setErasure, types)) { // Elements of a set can never be replaced, so treat them as @UnknownReplace. Even if // they are annotation @Modifiable in a stubfile. - return unknownReplaceSet; + return unknownReplace; + } else if (TypesUtils.isErasedSubtype(type, queueErasure, types) + && !TypesUtils.isErasedSubtype(type, linkedListErasure, types)) { + // Elements of a queue (but not LinkedList) can never be replaced, so treat them as + // @UnknownReplace. + return unknownReplace; + } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { + // Iterators cannot replace elements. + return unknownReplace; } return super.getBoundQualifiers(type); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index c072afebdcab..a7c6e653fc74 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -17,9 +17,12 @@ import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.QualifierUpperBounds; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.TypesUtils; /** The type factory for the Modifiability Checker. */ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -92,10 +95,8 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { } TypeMirror underlyingType = type.getUnderlyingType(); - Types types = getProcessingEnv().getTypeUtils(); - TypeMirror erasure = types.erasure(underlyingType); - if (types.isSubtype(erasure, mapEntryErasure)) { + if (TypesUtils.isErasedSubtype(underlyingType, mapEntryErasure, types)) { // Map.Entry: Drop G and S bits type.replaceAnnotation(UNKNOWN_SHRINK); } @@ -103,4 +104,22 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { return null; } } + + @Override + protected QualifierUpperBounds createQualifierUpperBounds() { + return new QualifierUpperBounds(this) { + private final AnnotationMirrorSet unknownShrink = + AnnotationMirrorSet.singleton(UNKNOWN_SHRINK); + + @Override + public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { + if (TypesUtils.isErasedSubtype(type, mapEntryErasure, types)) { + // Elements of a map entry can never be shrunk, so treat them as @UnknownShrink. Even if + // they are annotation @Growable in a stubfile. + return unknownShrink; + } + return super.getBoundQualifiers(type); + } + }; + } } From 57985baed9f2dbb852318a959cfad9e3407a2b6b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 11:56:13 -0800 Subject: [PATCH 063/308] Put error key in brackets --- checker/tests/README.md | 6 ++-- .../ConversionCategoryTest.java | 8 ++--- checker/tests/i18n-formatter/HasFormat.java | 12 +++---- checker/tests/i18n-formatter/I18nFormat.java | 12 +++---- .../i18n-formatter/I18nFormatForTest.java | 20 +++++------ checker/tests/i18n-formatter/IsFormat.java | 12 +++---- .../ManualExampleI18nFormatter.java | 10 +++--- checker/tests/i18n-formatter/Syntax.java | 34 +++++++++---------- .../dependenttypes/DependentTypesError.java | 2 +- .../tests/h1h2checker/TypeRefinement.java | 8 ++--- .../tests/reportmodifiers/TestModifiers.java | 2 +- .../tests/reporttreekinds/TestTreeKinds.java | 2 +- 12 files changed, 64 insertions(+), 64 deletions(-) diff --git a/checker/tests/README.md b/checker/tests/README.md index 6c9f75a50f79..9a61ee97e3e2 100644 --- a/checker/tests/README.md +++ b/checker/tests/README.md @@ -112,14 +112,14 @@ or by creating the test and observing the failure. To indicate the expected failure, insert the line ```java - // :: error: () + // :: error: [] ``` directly preceding the expected error line. If a warning rather than an error is expected, insert the line ```java - // :: warning: () + // :: warning: [] ``` If a stub parser warning is expected, insert the line @@ -132,7 +132,7 @@ If multiple errors are expected on a single line, duplicate everything except the "//" comment characters, as in ```java - // :: error: () :: error: () + // :: error: [] :: error: [] ``` If the expected failures line would be very long, you may break it across diff --git a/checker/tests/i18n-formatter/ConversionCategoryTest.java b/checker/tests/i18n-formatter/ConversionCategoryTest.java index fa49238c61b2..80bf3394848b 100644 --- a/checker/tests/i18n-formatter/ConversionCategoryTest.java +++ b/checker/tests/i18n-formatter/ConversionCategoryTest.java @@ -10,7 +10,7 @@ public static void main(String[] args) { @I18nFormat({I18nConversionCategory.NUMBER}) String s3 = "{0, number}"; @I18nFormat({I18nConversionCategory.NUMBER, I18nConversionCategory.NUMBER}) String s4 = "{1} {0, date}"; - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] s4 = "{0}"; @I18nFormat({I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER}) String s5 = "{0} and {1, number}"; @@ -48,7 +48,7 @@ public static void main(String[] args) { @I18nFormat({I18nConversionCategory.DATE}) String s13 = "{0, date} {0, date}"; - // :: error: (i18nformat.excess.arguments) :: error: [assignment] + // :: error: [i18nformat.excess.arguments] :: error: [assignment] @I18nFormat({I18nConversionCategory.GENERAL}) String b1 = "{1}"; // :: error: [assignment] @@ -60,10 +60,10 @@ public static void main(String[] args) { // :: error: [assignment] @I18nFormat({I18nConversionCategory.GENERAL}) String b4 = "{0, date}"; - // :: error: (i18nformat.excess.arguments) :: error: [assignment] + // :: error: [i18nformat.excess.arguments] :: error: [assignment] @I18nFormat({I18nConversionCategory.DATE}) String b5 = "{0, date} {1, date}"; - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] @I18nFormat({I18nConversionCategory.DATE, I18nConversionCategory.DATE}) String b6 = "{0, date}"; } } diff --git a/checker/tests/i18n-formatter/HasFormat.java b/checker/tests/i18n-formatter/HasFormat.java index 6cb59928deeb..007519794a23 100644 --- a/checker/tests/i18n-formatter/HasFormat.java +++ b/checker/tests/i18n-formatter/HasFormat.java @@ -9,11 +9,11 @@ void test1(String format) { if (I18nFormatUtil.hasFormat( format, I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER)) { MessageFormat.format(format, "S", 1); - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] MessageFormat.format(format, "S"); // :: error: [argument] MessageFormat.format(format, "S", "S"); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format(format, "S", 1, 2); } } @@ -21,7 +21,7 @@ void test1(String format) { void test2(String format) { if (!I18nFormatUtil.hasFormat( format, I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER)) { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(format, "S", 1); } } @@ -32,13 +32,13 @@ void test3(String format) { I18nConversionCategory.GENERAL, I18nConversionCategory.UNUSED, I18nConversionCategory.GENERAL)) { - // :: warning: (i18nformat.argument.unused) + // :: warning: [i18nformat.argument.unused] MessageFormat.format(format, "S", 1, "S"); } } void test4(String format) throws Exception { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(format, "S"); if (I18nFormatUtil.hasFormat(format, I18nConversionCategory.GENERAL)) { MessageFormat.format(format, "S"); @@ -55,7 +55,7 @@ void tes5(String format) { MessageFormat.format(format, "S"); MessageFormat.format(format, 1); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(format, 1); } } diff --git a/checker/tests/i18n-formatter/I18nFormat.java b/checker/tests/i18n-formatter/I18nFormat.java index 19f57d25cf9c..1232f963cb14 100644 --- a/checker/tests/i18n-formatter/I18nFormat.java +++ b/checker/tests/i18n-formatter/I18nFormat.java @@ -11,24 +11,24 @@ void test() { MessageFormat.format("{0, number}{1, number}", 1, 2); MessageFormat.format("{0, number}{0}", 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{0, number}'", new Date(12)); // Using {44444} here would yield: "invalid format string 44444 exceeds // the ArgumentIndex implementation limit". The hard limit is 10000. - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] MessageFormat.format("''{0, time, short}''{1}{2, time} {33, number}{4444}'{''''", 0); - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] MessageFormat.format("{0, number}{1, number}", 1); - // :: warning: (i18nformat.argument.unused) + // :: warning: [i18nformat.argument.unused] MessageFormat.format("{1, number}", 1, 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("{0, number}", 1, new Date()); - // :: warning: (i18nformat.indirect.arguments) + // :: warning: [i18nformat.indirect.arguments] MessageFormat.format("{0, number}", new Object[2]); MessageFormat.format("{0}", "S"); diff --git a/checker/tests/i18n-formatter/I18nFormatForTest.java b/checker/tests/i18n-formatter/I18nFormatForTest.java index 7a0a86e3cbb6..83eff2df7262 100644 --- a/checker/tests/i18n-formatter/I18nFormatForTest.java +++ b/checker/tests/i18n-formatter/I18nFormatForTest.java @@ -12,41 +12,41 @@ public static void main(String[] args) { A a1 = new A(); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] a1.methodA("{0, number", new Date(12)); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] a1.methodA("'{0{}", 1); a1.methodA("{0}", "A"); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] a(1, 1.2, "{0, number", 1.2, new Date(12)); a(1, 1.2, "{0, number}{1}", 1.2, 1, "A"); - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] a(1, 1.2, "{0, number}{1}", 1.2, 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] a(1, 1.2, "{0, number}{1}", 1.2, 1, "A", 2); b("{0, number}{1}", 1, "A"); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] b("{0, number", new Date(12)); b("{0, number}{1}", 1, "A"); b("{0}", "a string"); // :: error: [argument] b("{0, number}", "a string"); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] c("{0, number}{1}", 1, "A"); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] e(1, 2); f("{0}", 2); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] h("{0}", "a string"); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] i("{0}", "a string"); j("{0}"); diff --git a/checker/tests/i18n-formatter/IsFormat.java b/checker/tests/i18n-formatter/IsFormat.java index 129575397482..64a71952ce75 100644 --- a/checker/tests/i18n-formatter/IsFormat.java +++ b/checker/tests/i18n-formatter/IsFormat.java @@ -5,15 +5,15 @@ public class IsFormat { public static void test1(String cc) { if (!I18nFormatUtil.isFormat(cc)) { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); if (I18nFormatUtil.hasFormat(cc, I18nConversionCategory.GENERAL)) { MessageFormat.format(cc, "A"); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } } @@ -21,15 +21,15 @@ public static void test1(String cc) { public static void test2(String cc) { if (!I18nFormatUtil.isFormat(cc)) { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); if (I18nFormatUtil.hasFormat(cc, I18nConversionCategory.NUMBER)) { MessageFormat.format(cc, 1); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } } diff --git a/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java b/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java index f124f67deca3..3c14a7c76267 100644 --- a/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java +++ b/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java @@ -14,24 +14,24 @@ void m(boolean flag) { f = "{0, number, #.#} {1, date}"; // OK f = "{0, number} {1}"; // OK, GENERAL is weaker (less restrictive) than DATE f = "{0} {1, date}"; // OK, GENERAL is weaker (less restrictive) than NUMBER - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = "{0, number}"; // warning: last argument is ignored - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = "{0}"; // warning: last argument is ignored - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = flag ? "{0, number} {1}" : "{0, number}"; if (flag) { f = "{0, number} {1}"; } else { - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = "{0, number}"; } @I18nFormat({NUMBER, DATE}) String f2 = f; // :: error: [assignment] f = "{0, number} {1, number}"; // error: NUMBER is stronger (more restrictive) than DATE - // :: error: (i18nformat.excess.arguments) :: error: [assignment] + // :: error: [i18nformat.excess.arguments] :: error: [assignment] f = "{0} {1} {2}"; // error: too many arguments } } diff --git a/checker/tests/i18n-formatter/Syntax.java b/checker/tests/i18n-formatter/Syntax.java index ff67f839a5eb..68a24fc1eb24 100644 --- a/checker/tests/i18n-formatter/Syntax.java +++ b/checker/tests/i18n-formatter/Syntax.java @@ -5,35 +5,35 @@ public class Syntax { // Test 2.1.1: Missing '}' at end of message format (Unmatched braces in the pattern) public static void unmatchedBraces() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, number", new Date(12)); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0}{", 1); // good - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{0{}", 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{0{}'", 1); } // Test 2.1.2.1: The argument number needs to be an integer public static void integerRequired() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{{0}}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0.2}", 1); // good - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{{0}}'", 1); } // Test 2.1.2.2: The argument number can't be negative public static void nonNegativeRequired() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{-1, number}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{-123}", 1); // good @@ -42,7 +42,7 @@ public static void nonNegativeRequired() { // Test 2.1.3: Format Style required for choice format public static void formatStyleRequired() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, choice}", 1); // good @@ -51,9 +51,9 @@ public static void formatStyleRequired() { // Test 2.1.4: Wrong format Style public static void wrongFormatStyle() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, time, number}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, number, y.m.d}", 1); // good @@ -63,9 +63,9 @@ public static void wrongFormatStyle() { // Test 2.1.5: Unknown format type public static void unknownFormatType() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, general}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, fool}", 1); // good @@ -79,12 +79,12 @@ public static void unknownFormatType() { // Test 2.1.6: Invalid Subformat Pattern public static void invalidSubformatPattern() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, number, #.#.#}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, date, y.m.d.x}", new Date()); // This seems to be permitted by Java 23+. - // // :: error: (i18nformat.string) + // // :: error: [i18nformat.string] // MessageFormat.format("{0, choice, 0##zero}", 0); // good diff --git a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java index bdbc72d89ff0..4508cd4707fa 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java +++ b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java @@ -25,7 +25,7 @@ public class DependentTypesError { /** Regular expression for unparsing string representations of this class (gross). */ private static final Pattern ERROR_PATTERN = - Pattern.compile("\\[error for expression: (.*); error: (.*)\\]"); + Pattern.compile("\\[error for expression: (.*); error: [.*]\\]"); /** * Returns true if the given expression string is an error. That is, whether it is a string that diff --git a/framework/tests/h1h2checker/TypeRefinement.java b/framework/tests/h1h2checker/TypeRefinement.java index 682e865d37e9..f910e1a1bbd9 100644 --- a/framework/tests/h1h2checker/TypeRefinement.java +++ b/framework/tests/h1h2checker/TypeRefinement.java @@ -4,14 +4,14 @@ public class TypeRefinement { // :: warning: [cast.unsafe.constructor.invocation] @H1Top Object o = new @H1S1 Object(); - // :: error: (h1h2checker.h1invalid.forbidden) :: warning: [cast.unsafe.constructor.invocation] + // :: error: [h1h2checker.h1invalid.forbidden] :: warning: [cast.unsafe.constructor.invocation] @H1Top Object o2 = new @H1Invalid Object(); - // :: error: (h1h2checker.h1invalid.forbidden) + // :: error: [h1h2checker.h1invalid.forbidden] @H1Top Object o3 = getH1Invalid(); - // :: error: (h1h2checker.h1invalid.forbidden) + // :: error: [h1h2checker.h1invalid.forbidden] @H1Invalid Object getH1Invalid() { - // :: error: (h1h2checker.h1invalid.forbidden) :: warning: + // :: error: [h1h2checker.h1invalid.forbidden] :: warning: // (cast.unsafe.constructor.invocation) return new @H1Invalid Object(); } diff --git a/framework/tests/reportmodifiers/TestModifiers.java b/framework/tests/reportmodifiers/TestModifiers.java index f645645aa398..cde23a4c51c8 100644 --- a/framework/tests/reportmodifiers/TestModifiers.java +++ b/framework/tests/reportmodifiers/TestModifiers.java @@ -5,7 +5,7 @@ public class TestModifiers { void test() { class Inner { - // :: error: (Modifier.native) + // :: error: [Modifier.native] native void bad(); } } diff --git a/framework/tests/reporttreekinds/TestTreeKinds.java b/framework/tests/reporttreekinds/TestTreeKinds.java index dcf18aeb01f6..d191a7f27cc2 100644 --- a/framework/tests/reporttreekinds/TestTreeKinds.java +++ b/framework/tests/reporttreekinds/TestTreeKinds.java @@ -4,7 +4,7 @@ */ public class TestTreeKinds { void test(boolean a, boolean b) { - // :: error: (Tree.Kind.WHILE_LOOP) :: error: (Tree.Kind.CONDITIONAL_AND) + // :: error: [Tree.Kind.WHILE_LOOP] :: error: [Tree.Kind.CONDITIONAL_AND] while (a && b) {} if (b) {} } From 881e1739469642ff7a1751431757ff4de9b8967c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 12:06:30 -0800 Subject: [PATCH 064/308] Put error key in brackets --- .../modifiability/BasicModifiabilityTest.java | 28 ++++---- .../modifiability/BasicModifiableTest3.java | 2 +- .../CollectionModifiableTest.java | 22 +++--- .../CollectionsModifiableTest.java | 70 +++++++++---------- .../ConcurrentHashMapModifiableTest.java | 12 ++-- .../tests/modifiability/HierarchyTest.java | 34 ++++----- .../IdentityHashMapModifiableTest.java | 10 +-- .../modifiability/MapModifiableTest.java | 36 +++++----- .../modifiability/PolyModifiableTest.java | 30 ++++---- .../tests/modifiability/PolyShrinkTest.java | 22 +++--- .../modifiability/QueueModifiableTest.java | 4 +- .../modifiability/SetModifiableTest.java | 16 ++--- checker/tests/modifiability/SetTest.java | 8 +-- .../tests/modifiability/ThrowsUOETest.java | 28 ++++---- docs/manual/modifiability-checker.tex | 2 +- 15 files changed, 162 insertions(+), 162 deletions(-) diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 3a4bbda81106..69f7fb6f3291 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -16,37 +16,37 @@ void testBasicModifiable() { // Unmodifiable collections should not allow mutation @Unmodifiable List unmodifiableList = List.of("test1", "test2"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodifiableList.add("test3"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodifiableList.remove(0); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodifiableList.remove("test1"); } void testUnmodifiableFactoryMethods() { // These should be inferred as @Unmodifiable List list1 = List.of("a", "b"); - // :: error: (method.invocation) + // :: error: [method.invocation] list1.add("c"); // Explicit unmodifiable type + unmodifiable factory @Unmodifiable List list1u = List.of("x", "y"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable List cannotBeMod1 = List.of("m1", "m2"); List list2 = List.copyOf(new ArrayList<>()); - // :: error: (method.invocation) + // :: error: [method.invocation] list2.add("c"); - // :: error: (method.invocation) + // :: error: [method.invocation] list2.remove(0); List src = new ArrayList<>(); src.add("s"); @Unmodifiable List list2u = List.copyOf(src); - // :: error: (assignment) + // :: error: [assignment] @Modifiable List cannotBeMod2 = List.copyOf(src); } @@ -80,7 +80,7 @@ void testIteratorPolymorphic( @Unmodifiable Iterator itUnmod = unmodList.iterator(); // Trying to treat the unmodifiable iterator as modifiable should be rejected. - // :: error: (assignment) + // :: error: [assignment] @Modifiable Iterator badIt = unmodList.iterator(); } @@ -92,18 +92,18 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List mod.removeAll(other); mod.retainAll(other); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.addAll(other); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.removeAll(other); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.retainAll(other); } void testReplaceAll(@Modifiable List mod, @Unmodifiable List unmod) { mod.replaceAll(s -> s + "!"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.replaceAll(s -> s + "!"); } @@ -111,7 +111,7 @@ void testSubList(@Modifiable List mod, @Unmodifiable List unmod) List sub = mod.subList(0, mod.size()); sub.clear(); // OK because sub is modifiable - // :: error: (assignment) + // :: error: [assignment] @Modifiable List sub2 = unmod.subList(0, unmod.size()); } } diff --git a/checker/tests/modifiability/BasicModifiableTest3.java b/checker/tests/modifiability/BasicModifiableTest3.java index b1a02e1ee370..69add9bccbb9 100644 --- a/checker/tests/modifiability/BasicModifiableTest3.java +++ b/checker/tests/modifiability/BasicModifiableTest3.java @@ -17,7 +17,7 @@ boolean my_add(@Modifiable @GuardSatisfied List this_one, E e) { void testBasicModifiable() { // Unmodifiable collections should not allow mutation @Unmodifiable List unmodifiableList = List.of("test1", "test2"); - // :: error: (argument) + // :: error: [argument] my_add(unmodifiableList, "test3"); } } diff --git a/checker/tests/modifiability/CollectionModifiableTest.java b/checker/tests/modifiability/CollectionModifiableTest.java index d08b337064f1..01f458bf19c4 100644 --- a/checker/tests/modifiability/CollectionModifiableTest.java +++ b/checker/tests/modifiability/CollectionModifiableTest.java @@ -21,19 +21,19 @@ void testMutatingMethods() { mod.removeIf(s -> s.isEmpty()); @Unmodifiable Collection unmod = List.of("x", "y"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.add("z"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.addAll(List.of("z")); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.clear(); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.remove("x"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.removeAll(List.of("x")); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.retainAll(List.of("y")); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.removeIf(s -> true); } @@ -59,13 +59,13 @@ void testFactoryAssignments() { Collection inferred = List.of("k"); @Unmodifiable Collection explicitUnmod = List.of("k"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Collection cannotBeMod1 = List.of("m1"); List src = new ArrayList<>(); src.add("s"); Collection copy = List.copyOf(src); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Collection cannotBeMod2 = List.copyOf(src); } @@ -74,10 +74,10 @@ void testAnnotatedLocalsFromParams( @Unmodifiable Collection unmod, @UnknownModifiability Collection unknown) { mod.add("p"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.add("p"); // depending on defaults, this may be an error: - // :: error: (method.invocation) + // :: error: [method.invocation] unknown.add("q"); } } diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index 53681b9c9e21..6a0b56b86c96 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -27,7 +27,7 @@ void testAddAll() { Collections.addAll(mod, "a", "b"); @Unmodifiable Collection unmod = Collections.unmodifiableCollection(mod); - // :: error: (argument) + // :: error: [argument] Collections.addAll(unmod, "c"); } @@ -83,7 +83,7 @@ void testCopy() { Collections.copy(dest, src); @Unmodifiable List unmodDest = Collections.unmodifiableList(dest); - // :: error: (argument) + // :: error: [argument] Collections.copy(unmodDest, src); } @@ -95,21 +95,21 @@ void testDisjoint() { void testEmptyCollections() { @Unmodifiable List l = Collections.emptyList(); - // :: error: (method.invocation) + // :: error: [method.invocation] l.add("a"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable List modL = Collections.emptyList(); @Unmodifiable Set s = Collections.emptySet(); - // :: error: (method.invocation) + // :: error: [method.invocation] s.add("a"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Set modS = Collections.emptySet(); @Unmodifiable Map m = Collections.emptyMap(); - // :: error: (method.invocation) + // :: error: [method.invocation] m.put("a", "b"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Map modM = Collections.emptyMap(); @Modifiable Iterator it = Collections.emptyIterator(); @@ -121,7 +121,7 @@ void testFill() { Collections.fill(list, "b"); @Unmodifiable List unmod = Collections.unmodifiableList(list); - // :: error: (argument) + // :: error: [argument] Collections.fill(unmod, "c"); } @@ -152,9 +152,9 @@ void testMinMax() { void testNCopies() { @Unmodifiable List l = Collections.nCopies(5, "a"); - // :: error: (method.invocation) + // :: error: [method.invocation] l.add("b"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable List mod = Collections.nCopies(5, "a"); } @@ -164,7 +164,7 @@ void testReplaceAll() { Collections.replaceAll(list, "a", "b"); @Unmodifiable List unmod = Collections.unmodifiableList(list); - // :: error: (argument) + // :: error: [argument] Collections.replaceAll(unmod, "a", "b"); } @@ -174,7 +174,7 @@ void testReverse() { Collections.reverse(list); @Unmodifiable List unmod = Collections.unmodifiableList(list); - // :: error: (argument) + // :: error: [argument] Collections.reverse(unmod); } @@ -184,7 +184,7 @@ void testRotate() { Collections.rotate(list, 1); @Unmodifiable List unmod = Collections.unmodifiableList(list); - // :: error: (argument) + // :: error: [argument] Collections.rotate(unmod, 1); } @@ -195,29 +195,29 @@ void testShuffle() { Collections.shuffle(list, new Random()); @Unmodifiable List unmod = Collections.unmodifiableList(list); - // :: error: (argument) + // :: error: [argument] Collections.shuffle(unmod); - // :: error: (argument) + // :: error: [argument] Collections.shuffle(unmod, new Random()); } void testSingleton() { @Unmodifiable Set s = Collections.singleton("a"); - // :: error: (method.invocation) + // :: error: [method.invocation] s.add("b"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Set modS = Collections.singleton("a"); @Unmodifiable List l = Collections.singletonList("a"); - // :: error: (method.invocation) + // :: error: [method.invocation] l.add("b"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable List modL = Collections.singletonList("a"); @Unmodifiable Map m = Collections.singletonMap("a", "b"); - // :: error: (method.invocation) + // :: error: [method.invocation] m.put("c", "d"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Map modM = Collections.singletonMap("a", "b"); } @@ -228,9 +228,9 @@ void testSort() { Collections.sort(list, Comparator.naturalOrder()); @Unmodifiable List unmod = Collections.unmodifiableList(list); - // :: error: (argument) + // :: error: [argument] Collections.sort(unmod); - // :: error: (argument) + // :: error: [argument] Collections.sort(unmod, Comparator.naturalOrder()); } @@ -241,7 +241,7 @@ void testSwap() { Collections.swap(mod, 0, 1); @Unmodifiable List unmod = Collections.unmodifiableList(mod); - // :: error: (argument) + // :: error: [argument] Collections.swap(unmod, 0, 1); } @@ -259,40 +259,40 @@ void testSynchronizedWrappers() { void testUnmodifiableWrappers() { @Modifiable Collection c = new ArrayList<>(); @Unmodifiable Collection unmodC = Collections.unmodifiableCollection(c); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodC.add("a"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Collection modC = Collections.unmodifiableCollection(c); @Modifiable List l = new ArrayList<>(); @Unmodifiable List unmodL = Collections.unmodifiableList(l); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodL.add("a"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable List modL = Collections.unmodifiableList(l); @Modifiable Set s = new HashSet<>(); @Unmodifiable Set unmodS = Collections.unmodifiableSet(s); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodS.add("a"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Set modS = Collections.unmodifiableSet(s); @Modifiable Map m = new HashMap<>(); @Unmodifiable Map unmodM = Collections.unmodifiableMap(m); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodM.put("a", "b"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Map modM = Collections.unmodifiableMap(m); @Modifiable SortedSet ss = new TreeSet<>(); @Unmodifiable SortedSet unmodSS = Collections.unmodifiableSortedSet(ss); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodSS.add("a"); @Modifiable SortedMap sm = new TreeMap<>(); @Unmodifiable SortedMap unmodSM = Collections.unmodifiableSortedMap(sm); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodSM.put("a", "b"); } } diff --git a/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java b/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java index ce16912c970d..d0021d020aeb 100644 --- a/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java +++ b/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java @@ -36,7 +36,7 @@ void testBulkOperations() { map.forEachEntry( 1, entry -> { - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("3"); }); @@ -44,7 +44,7 @@ void testBulkOperations() { map.searchEntries( 1, entry -> { - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("3"); return "result"; }); @@ -53,7 +53,7 @@ void testBulkOperations() { map.reduceEntries( 1, entry -> { - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("3"); return "result"; }, @@ -63,7 +63,7 @@ void testBulkOperations() { map.reduceEntriesToDouble( 1, entry -> { - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("3"); return 0.0; }, @@ -74,7 +74,7 @@ void testBulkOperations() { map.reduceEntriesToInt( 1, entry -> { - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("3"); return 0; }, @@ -85,7 +85,7 @@ void testBulkOperations() { map.reduceEntriesToLong( 1, entry -> { - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("3"); return 0L; }, diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index d7396a84b1d8..428bd4f19f0d 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -26,7 +26,7 @@ class HierarchyTest { void testGrow(@Growable Object g, @UnknownGrow Object u) { // Growable <: UnknownGrow (bottom to top is always OK) @UnknownGrow Object u1 = g; - // :: error: (assignment) + // :: error: [assignment] @Growable Object g1 = u; // UnknownGrow !<: Growable } @@ -35,7 +35,7 @@ void testGrow(@Growable Object g, @UnknownGrow Object u) { // ============================================================ void testShrink(@Shrinkable Object s, @UnknownShrink Object u) { @UnknownShrink Object u1 = s; - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Object s1 = u; } @@ -44,7 +44,7 @@ void testShrink(@Shrinkable Object s, @UnknownShrink Object u) { // ============================================================ void testReplace(@Replaceable Object r, @UnknownReplace Object u) { @UnknownReplace Object u1 = r; - // :: error: (assignment) + // :: error: [assignment] @Replaceable Object r1 = u; } @@ -65,42 +65,42 @@ void testCombinations( @Growable Object gv1 = gsr; // G+S+R <: G @Growable Object gv2 = gs; // G+S <: G @Growable Object gv3 = gr; // G+R <: G - // :: error: (assignment) + // :: error: [assignment] @Growable Object gv4 = sr; // S+R: no G bit @Growable Object gv5 = g; - // :: error: (assignment) + // :: error: [assignment] @Growable Object gv6 = s; - // :: error: (assignment) + // :: error: [assignment] @Growable Object gv7 = r; - // :: error: (assignment) + // :: error: [assignment] @Growable Object gv8 = none; // ── Assignments to @Shrinkable ──────────────────────────── @Shrinkable Object sv1 = gsr; @Shrinkable Object sv2 = gs; - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Object sv3 = gr; // G+R: no S bit @Shrinkable Object sv4 = sr; - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Object sv5 = g; @Shrinkable Object sv6 = s; - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Object sv7 = r; - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Object sv8 = none; // ── Assignments to @Replaceable ─────────────────────────── @Replaceable Object rv1 = gsr; - // :: error: (assignment) + // :: error: [assignment] @Replaceable Object rv2 = gs; // G+S: no R bit @Replaceable Object rv3 = gr; @Replaceable Object rv4 = sr; - // :: error: (assignment) + // :: error: [assignment] @Replaceable Object rv5 = g; - // :: error: (assignment) + // :: error: [assignment] @Replaceable Object rv6 = s; @Replaceable Object rv7 = r; - // :: error: (assignment) + // :: error: [assignment] @Replaceable Object rv8 = none; // ── Assignments to full @UnknownGrow @UnknownShrink @UnknownReplace (top in all) ── @@ -136,11 +136,11 @@ void testAliases( @UnknownModifiability Object unknown1 = mod; // @Unmodifiable !<: @Growable (top !<: bottom in Grow hierarchy) - // :: error: (assignment) + // :: error: [assignment] @Growable Object g2 = unmod; // @Unmodifiable !<: @Modifiable - // :: error: (assignment) + // :: error: [assignment] @Modifiable Object mod1 = unmod; } } diff --git a/checker/tests/modifiability/IdentityHashMapModifiableTest.java b/checker/tests/modifiability/IdentityHashMapModifiableTest.java index 8fded55f50aa..26a11c7aa9e9 100644 --- a/checker/tests/modifiability/IdentityHashMapModifiableTest.java +++ b/checker/tests/modifiability/IdentityHashMapModifiableTest.java @@ -19,13 +19,13 @@ void testIdentityHashMap() { // @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); // // Views (keySet, values, entrySet) usually throw UOE on add operations - // // :: error: (method.invocation) + // // :: error: [method.invocation] // identityMap.keySet().add("newKey"); - // // :: error: (method.invocation) + // // :: error: [method.invocation] // identityMap.values().add("newValue"); - // // :: error: (method.invocation) + // // :: error: [method.invocation] // identityMap.entrySet().add(new AbstractMap.SimpleEntry<>("newKey", "newValue")); // // However, removal is supported, so these should be allowed: @@ -45,7 +45,7 @@ void testEntries() { // the following method is allowed and works at runtime, but the current type system throws // an error because we could only make entrySet.iterator() and entrySet.stream() return the // same mutability. - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("modified"); // OK } @@ -53,7 +53,7 @@ void testEntries() { if (!identityMap.isEmpty()) { Map.Entry entry = identityMap.entrySet().stream().findFirst().get(); // This throws UOE at runtime. - // :: error: (method.invocation) + // :: error: [method.invocation] entry.setValue("modified"); } } diff --git a/checker/tests/modifiability/MapModifiableTest.java b/checker/tests/modifiability/MapModifiableTest.java index 601511066670..b48493b56289 100644 --- a/checker/tests/modifiability/MapModifiableTest.java +++ b/checker/tests/modifiability/MapModifiableTest.java @@ -27,31 +27,31 @@ void testBasicMutatingOperations() { mod.clear(); @Unmodifiable Map unmod = Map.of("x", 1); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.put("y", 2); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.putIfAbsent("z", 3); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.putAll(Map.of("a", 1)); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.remove("x"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.remove("x", 1); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.replace("x", 2); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.replace("x", 2, 3); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.replaceAll((k, v) -> v + 1); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.compute("g", (k, v) -> 1); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.computeIfAbsent("h", k -> 2); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.computeIfPresent("h", (k, v) -> v + 1); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.merge("i", 1, (oldV, newV) -> oldV + newV); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.clear(); } @@ -76,13 +76,13 @@ void testFactoryMethodsAndAssignments() { Map inferred = Map.of("k", 1); @Unmodifiable Map explicitUnmod = Map.of("k", 1); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Map cannotBeMod1 = Map.of("m1", 1); Map src = new HashMap<>(); src.put("s", 1); Map copy = Map.copyOf(src); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Map cannotBeMod2 = Map.copyOf(src); } @@ -90,7 +90,7 @@ void testEntryModifiabilityLocals(@Modifiable Entry modEntry) { modEntry.setValue(2); @Unmodifiable Entry immEntry = Map.entry("k", 1); - // :: error: (method.invocation) + // :: error: [method.invocation] immEntry.setValue(5); } @@ -100,10 +100,10 @@ void testAnnotatedLocalsFromParams( @Unmodifiable Map unmodSrc) { modSrc.put("p", 1); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodSrc.put("p", 1); // depending on defaults, this may be an error: - // :: error: (method.invocation) + // :: error: [method.invocation] any.put("q", 2); } } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 0edc603998b6..9f6e5db69e08 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -39,21 +39,21 @@ void testPoly( // Identity on @Growable @Shrinkable (G+S, R=UnknownReplace) // ============================================================ @Growable @Shrinkable List gs1 = identity(gs); // OK - // :: error: (assignment) + // :: error: [assignment] @Modifiable List gs2 = identity(gs); // Error: R=Unknown !<: Replaceable @Growable List gs3 = identity(gs); // OK: G+S+UnknownR <: G @Shrinkable List gs4 = identity(gs); // OK: G+S+UnknownR <: S - // :: error: (assignment) + // :: error: [assignment] @Replaceable List gs5 = identity(gs); // Error: UnknownR !<: Replaceable // ============================================================ // Identity on @Growable @Replaceable (G+R, S=UnknownShrink) // ============================================================ @Growable @Replaceable List gr1 = identity(gr); // OK - // :: error: (assignment) + // :: error: [assignment] @Modifiable List gr2 = identity(gr); // Error: S=Unknown !<: Shrinkable @Growable List gr3 = identity(gr); // OK - // :: error: (assignment) + // :: error: [assignment] @Shrinkable List gr4 = identity(gr); // Error: S=Unknown @Replaceable List gr5 = identity(gr); // OK @@ -61,9 +61,9 @@ void testPoly( // Identity on @Shrinkable @Replaceable (S+R, G=UnknownGrow) // ============================================================ @Shrinkable @Replaceable List sr1 = identity(sr); // OK - // :: error: (assignment) + // :: error: [assignment] @Modifiable List sr2 = identity(sr); // Error: G=Unknown !<: Growable - // :: error: (assignment) + // :: error: [assignment] @Growable List sr3 = identity(sr); // Error: G=Unknown @Shrinkable List sr4 = identity(sr); // OK @Replaceable List sr5 = identity(sr); // OK @@ -72,43 +72,43 @@ void testPoly( // Identity on @Growable (G only; S=Unknown, R=Unknown) // ============================================================ @Growable List g1 = identity(g); // OK - // :: error: (assignment) + // :: error: [assignment] @Growable @Shrinkable List g2 = identity(g); // Error: S=Unknown - // :: error: (assignment) + // :: error: [assignment] @Modifiable List g3 = identity(g); // Error: S=Unknown, R=Unknown // ============================================================ // Identity on @Shrinkable (S only; G=Unknown, R=Unknown) // ============================================================ @Shrinkable List s1 = identity(s); // OK - // :: error: (assignment) + // :: error: [assignment] @Growable @Shrinkable List s2 = identity(s); // Error: G=Unknown - // :: error: (assignment) + // :: error: [assignment] @Modifiable List s3 = identity(s); // Error // ============================================================ // Identity on @Replaceable (R only; G=Unknown, S=Unknown) // ============================================================ @Replaceable List r1 = identity(r); // OK - // :: error: (assignment) + // :: error: [assignment] @Growable @Replaceable List r2 = identity(r); // Error: G=Unknown - // :: error: (assignment) + // :: error: [assignment] @Modifiable List r3 = identity(r); // Error // ============================================================ // Identity on @UnknownModifiability (all tops) // ============================================================ @UnknownModifiability List u1 = identity(unknown); // OK - // :: error: (assignment) + // :: error: [assignment] @Modifiable List u2 = identity(unknown); // Error - // :: error: (assignment) + // :: error: [assignment] @Growable List u3 = identity(unknown); // Error // ============================================================ // Identity on @Unmodifiable (alias for all tops) // ============================================================ @Unmodifiable List unmod1 = identity(unmod); // OK - // :: error: (assignment) + // :: error: [assignment] @Modifiable List unmod2 = identity(unmod); // Error } } diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java index ffb9d0a81495..f1191ee1217b 100644 --- a/checker/tests/modifiability/PolyShrinkTest.java +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -9,15 +9,15 @@ void testMapKeySet() { // Modifiable map: keySet() returns @Shrinkable. Cannot add or replace. @Modifiable Map modMap = new java.util.HashMap<>(); @Shrinkable Set modKeySet = modMap.keySet(); - // :: error: (assignment) + // :: error: [assignment] @Growable Set modKeySetG = modMap.keySet(); // Cannot add // Unmodifiable map: keySet() returns @Unmodifiable. Cannot shrink, add, or replace. @Unmodifiable Map unmodMap = Map.of("a", "b"); @Unmodifiable Set unmodKeySet = unmodMap.keySet(); - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Set unmodKeySetS = unmodMap.keySet(); // Cannot shrink - // :: error: (assignment) + // :: error: [assignment] @Growable Set unmodKeySetG = unmodMap.keySet(); // Cannot add } @@ -25,19 +25,19 @@ void testMapValues() { // Modifiable map: values() returns @Shrinkable. Cannot add or replace. @Modifiable Map modMap = new java.util.HashMap<>(); @Shrinkable Collection modVals = modMap.values(); - // :: error: (assignment) + // :: error: [assignment] @Growable Collection modValsG = modMap.values(); // Cannot add - // :: error: (assignment) + // :: error: [assignment] @Replaceable Collection modValsR = modMap.values(); // Cannot replace // Unmodifiable map: values() returns @Unmodifiable. Cannot shrink, add, or replace. @Unmodifiable Map unmodMap = Map.of("a", "b"); @Unmodifiable Collection unmodVals = unmodMap.values(); - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Collection unmodValsS = unmodMap.values(); // Cannot shrink - // :: error: (assignment) + // :: error: [assignment] @Growable Collection unmodValsG = unmodMap.values(); // Cannot add - // :: error: (assignment) + // :: error: [assignment] @Replaceable Collection unmodValsR = unmodMap.values(); // Cannot replace } @@ -47,15 +47,15 @@ void testMapEntrySet() { // Modifiable map: Set is @Shrinkable, Entry is @Modifiable. @Modifiable Map modMap = new java.util.HashMap<>(); @Shrinkable Set> modEntries = modMap.entrySet(); - // :: error: (assignment) + // :: error: [assignment] @Growable Set> modEntriesG = modMap.entrySet(); // Set cannot add // Unmodifiable map: Set is @Unmodifiable, Entry is @Unmodifiable. @Unmodifiable Map unmodMap = Map.of("a", "b"); @Unmodifiable Set> unmodEntries = unmodMap.entrySet(); - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Set> unmodEntriesS = unmodMap.entrySet(); - // :: error: (assignment) + // :: error: [assignment] @Growable Set> unmodEntriesG = unmodMap.entrySet(); } } diff --git a/checker/tests/modifiability/QueueModifiableTest.java b/checker/tests/modifiability/QueueModifiableTest.java index 43b22d37148c..a1a1642def2d 100644 --- a/checker/tests/modifiability/QueueModifiableTest.java +++ b/checker/tests/modifiability/QueueModifiableTest.java @@ -117,7 +117,7 @@ void testUnmodifiableWrapper() { // Case 1: Modifying an unmodifiable Collection view of a Queue Collection unmodifiableQueue = Collections.unmodifiableCollection(queue); - // :: error: (method.invocation) + // :: error: [method.invocation] unmodifiableQueue.add("This will fail"); // Case 2: Iterator remove on Unmodifiable Collection @@ -125,7 +125,7 @@ void testUnmodifiableWrapper() { Iterator it = unmodifiableQueue2.iterator(); if (it.hasNext()) { it.next(); - // :: error: (method.invocation) + // :: error: [method.invocation] it.remove(); } } diff --git a/checker/tests/modifiability/SetModifiableTest.java b/checker/tests/modifiability/SetModifiableTest.java index e9a118e4063a..8fc819d5eb18 100644 --- a/checker/tests/modifiability/SetModifiableTest.java +++ b/checker/tests/modifiability/SetModifiableTest.java @@ -17,17 +17,17 @@ void testMutatingMethods() { mod.clear(); @Unmodifiable Set unmod = Set.of("x", "y"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.add("z"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.addAll(Set.of("z")); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.remove("x"); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.removeAll(Set.of("x")); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.retainAll(Set.of("y")); - // :: error: (method.invocation) + // :: error: [method.invocation] unmod.clear(); } @@ -49,13 +49,13 @@ void testFactoryAssignments() { Set inferred = Set.of("k"); @Unmodifiable Set explicitUnmod = Set.of("k"); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Set cannotBeMod1 = Set.of("m1"); Set src = new HashSet<>(); src.add("s"); Set copy = Set.copyOf(src); - // :: error: (assignment) + // :: error: [assignment] @Modifiable Set cannotBeMod2 = Set.copyOf(src); } } diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 9565cb711cd1..f0781670f82f 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -59,12 +59,12 @@ void testSetGrowShrinkPreserved( // @Growable Set: Grow=G, Shrink=Unknown, Replace=Unknown @Growable Set g1 = gs; // OK: G+S <: G - // :: error: (assignment) + // :: error: [assignment] @Growable Set g2 = shrinkable; // Error: S only, no G // @Shrinkable Set: Grow=Unknown, Shrink=S, Replace=Unknown @Shrinkable Set s1 = gs; // OK: G+S <: S - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Set s2 = growable; // Error: G only, no S } @@ -117,7 +117,7 @@ void testEntryAssignment( // Cross-group: Replace !<: UnknownReplace (wrong direction) is NOT an error — // but UnknownReplace !<: Replaceable IS an error: - // :: error: (assignment) + // :: error: [assignment] Map.@Replaceable Entry bad1 = growable; // Error: R=Unknown !<: Replaceable } @@ -159,7 +159,7 @@ void testIteratorAssignment( @Growable @Shrinkable Iterator gs2 = shrinkable; @Shrinkable @Replaceable Iterator sr2 = growShrink; - // :: error: (assignment) + // :: error: [assignment] @Shrinkable Iterator bad1 = growable; // Error: Shrink=Unknown !<: Shrinkable } } diff --git a/checker/tests/modifiability/ThrowsUOETest.java b/checker/tests/modifiability/ThrowsUOETest.java index f6fc2cb26656..7d0808667a21 100644 --- a/checker/tests/modifiability/ThrowsUOETest.java +++ b/checker/tests/modifiability/ThrowsUOETest.java @@ -9,10 +9,10 @@ public class ThrowsUOETest { void testSortedSet(SortedSet s) { - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] s.addFirst("foo"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] s.addLast("bar"); } @@ -21,50 +21,50 @@ void testImplementation(TreeSet ts) { // We verify that calling them on a concrete implementation also triggers the // warning. - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] ts.addFirst("foo"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] ts.addLast("bar"); } void testSortedMap(SortedMap m) { - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putFirst("foo", "bar"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putLast("baz", "qux"); } void testImplementation(TreeMap tm) { - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] tm.putFirst("foo", "bar"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] tm.putLast("baz", "qux"); } void testNavigableMap(NavigableMap m) { - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putFirst("foo", "bar"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putLast("baz", "qux"); } void testConcurrentNavigableMap(ConcurrentNavigableMap m) { - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putFirst("foo", "bar"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putLast("baz", "qux"); } void testConcurrentSkipListMap(ConcurrentSkipListMap m) { - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putFirst("foo", "bar"); - // :: error: (usage.throws.uoe) + // :: error: [usage.throws.uoe] m.putLast("baz", "qux"); } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 3e7ab4443481..0f9818e09450 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -367,7 +367,7 @@ void testPoly(@Modifiable List mod, @Growable @Shrinkable List gs) { @Modifiable List m = wrap(mod); // OK @Growable @Shrinkable List gs2 = wrap(gs); // OK - // :: error: (assignment) + // :: error: [assignment] @Modifiable List bad = wrap(gs); // Error: gs has @UnknownReplace } } From 7668ccc633ff7e99503bc5c5dbd9935485988357 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 13:36:28 -0800 Subject: [PATCH 065/308] Undo a change --- .../framework/util/dependenttypes/DependentTypesError.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java index 4508cd4707fa..bdbc72d89ff0 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java +++ b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java @@ -25,7 +25,7 @@ public class DependentTypesError { /** Regular expression for unparsing string representations of this class (gross). */ private static final Pattern ERROR_PATTERN = - Pattern.compile("\\[error for expression: (.*); error: [.*]\\]"); + Pattern.compile("\\[error for expression: (.*); error: (.*)\\]"); /** * Returns true if the given expression string is an error. That is, whether it is a string that From cb5f029275bd70f3ee15a03180a7fdc1ffb969e8 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 13:51:06 -0800 Subject: [PATCH 066/308] Another pair of brackets --- framework/tests/h1h2checker/TypeRefinement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/tests/h1h2checker/TypeRefinement.java b/framework/tests/h1h2checker/TypeRefinement.java index f910e1a1bbd9..02147e9f5114 100644 --- a/framework/tests/h1h2checker/TypeRefinement.java +++ b/framework/tests/h1h2checker/TypeRefinement.java @@ -12,7 +12,7 @@ public class TypeRefinement { // :: error: [h1h2checker.h1invalid.forbidden] @H1Invalid Object getH1Invalid() { // :: error: [h1h2checker.h1invalid.forbidden] :: warning: - // (cast.unsafe.constructor.invocation) + // [cast.unsafe.constructor.invocation] return new @H1Invalid Object(); } } From 775a2dbcaef432832467c4d94d64941a9f4fb178 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 18:13:12 -0800 Subject: [PATCH 067/308] Add space around heading --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b6e97214b966..8701e8ab92ed 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,7 @@ + ## Version 3.54.1 (2026-04-02) ### User-visible changes From d957ab19f29f10ae4b3bd94b6bbf81fbb7dc99db Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 5 Mar 2026 17:26:21 -0800 Subject: [PATCH 068/308] copied ical4j stub into grow, replace, and shrink. --- .../checker/modifiability/grow/GrowChecker.java | 2 +- .../checker/modifiability/grow/ical4j.astub | 7 +++++++ .../checker/modifiability/replace/ReplaceChecker.java | 2 +- .../checker/modifiability/replace/ical4j.astub | 7 +++++++ .../checker/modifiability/shrink/ShrinkChecker.java | 2 +- .../checker/modifiability/shrink/ical4j.astub | 7 +++++++ 6 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 71c21d708502..746935189773 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -26,7 +26,7 @@ Map.Entry.class, Collections.class }) -@StubFiles({"../ical4j.astub"}) +@StubFiles({"ical4j.astub"}) @SuppressWarningsPrefix({"Growable", "UnknownGrow"}) public class GrowChecker extends BaseTypeChecker { /** Creates a Grow checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub new file mode 100644 index 000000000000..8bac550ff5b4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.modifiability.qual.*; + +package net.fortuna.ical4j.model; + +@Modifiable class ComponentList {} + +@Modifiable class PropertyList {} \ No newline at end of file diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index c4c81fe4357f..0dd106cf6ab9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -26,7 +26,7 @@ Map.Entry.class, Collections.class }) -@StubFiles({"../ical4j.astub"}) +@StubFiles({"ical4j.astub"}) @SuppressWarningsPrefix({"Replaceable", "UnknownReplace"}) public class ReplaceChecker extends BaseTypeChecker { /** Creates a Replace checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub new file mode 100644 index 000000000000..8bac550ff5b4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.modifiability.qual.*; + +package net.fortuna.ical4j.model; + +@Modifiable class ComponentList {} + +@Modifiable class PropertyList {} \ No newline at end of file diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index 99a270bc703c..c22502a8fce3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -26,7 +26,7 @@ Map.Entry.class, Collections.class }) -@StubFiles({"../ical4j.astub"}) +@StubFiles({"ical4j.astub"}) @SuppressWarningsPrefix({"Shrinkable", "UnknownShrink"}) public class ShrinkChecker extends BaseTypeChecker { /** Creates a Shrink checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub new file mode 100644 index 000000000000..8bac550ff5b4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.modifiability.qual.*; + +package net.fortuna.ical4j.model; + +@Modifiable class ComponentList {} + +@Modifiable class PropertyList {} \ No newline at end of file From 7218237e47b7a84297d4e9f2d270d2a195a326ed Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 16 Mar 2026 12:22:26 -0700 Subject: [PATCH 069/308] added test for arraylist cast. --- .../IdentityHashMapModifiableTest.java | 12 +++---- .../modifiability/UnmodArrayListTest.java | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 checker/tests/modifiability/UnmodArrayListTest.java diff --git a/checker/tests/modifiability/IdentityHashMapModifiableTest.java b/checker/tests/modifiability/IdentityHashMapModifiableTest.java index 26a11c7aa9e9..533e39b76e71 100644 --- a/checker/tests/modifiability/IdentityHashMapModifiableTest.java +++ b/checker/tests/modifiability/IdentityHashMapModifiableTest.java @@ -39,21 +39,19 @@ void testEntries() { identityMap.put("k", "v"); // Iterator returns mutable entries - Iterator> it = identityMap.entrySet().iterator(); + Iterator> it = identityMap.entrySet().iterator(); if (it.hasNext()) { Map.Entry entry = it.next(); - // the following method is allowed and works at runtime, but the current type system throws - // an error because we could only make entrySet.iterator() and entrySet.stream() return the - // same mutability. - // :: error: [method.invocation] + // the following method is allowed and works at runtime entry.setValue("modified"); // OK } // Stream/Spliterator returns immutable entries if (!identityMap.isEmpty()) { Map.Entry entry = identityMap.entrySet().stream().findFirst().get(); - // This throws UOE at runtime. - // :: error: [method.invocation] + // This throws UOE at runtime, but we could only make entrySet.iterator() and + // entrySet.stream() return the + // same mutability. So this is potentially unsound. entry.setValue("modified"); } } diff --git a/checker/tests/modifiability/UnmodArrayListTest.java b/checker/tests/modifiability/UnmodArrayListTest.java new file mode 100644 index 000000000000..fe19c7b75fba --- /dev/null +++ b/checker/tests/modifiability/UnmodArrayListTest.java @@ -0,0 +1,32 @@ +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +/** + * Demonstrates a known soundness hole: casting an @Unmodifiable list to a concrete @Modifiable + * class (ArrayList) bypasses the checker's protection. + * + *

The checker sees ArrayList as @Modifiable by default, so the downcast is treated as "safe" and + * the subsequent add() call raises no warning — even though the underlying object is an + * unmodifiable wrapper that will throw UnsupportedOperationException at runtime. + */ +public class UnmodArrayListTest { + + void testUnmodifiableCastEscape() { + ArrayList myList = new ArrayList<>(); + myList.add("hello"); + + // Checker correctly infers @Unmodifiable here. + @Unmodifiable List view = Collections.unmodifiableList(myList); + + // Because ArrayList is treated as @Modifiable by the checker, this downcast is considered + // "safe" — no cast.unsafe warning is issued, even though the underlying object is an + // unmodifiable wrapper and will throw ClassCastException at runtime. + ArrayList backToMod = (ArrayList) view; + + // The checker allows this because backToMod is typed as @Modifiable ArrayList. + // :: error: [method.invocation] + backToMod.add("Boom"); + } +} From f37afe77e3bf49aa9f3f0c3b01f4c7cfc13d1818 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 16 Mar 2026 14:17:45 -0700 Subject: [PATCH 070/308] fix formatting of type annotations --- buildSrc/src/main/groovy/cf-spotless.gradle | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index aa6a3a47283f..f89efd648852 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -89,6 +89,19 @@ spotless { googleJavaFormat(libs.google.java.format.get().version) } formatAnnotations() + .addTypeAnnotation("Growable") + .addTypeAnnotation("Modifiable") + .addTypeAnnotation("PolyGrow") + .addTypeAnnotation("PolyModifiable") + .addTypeAnnotation("PolyReplace") + .addTypeAnnotation("PolyShrink") + .addTypeAnnotation("Replaceable") + .addTypeAnnotation("Shrinkable") + .addTypeAnnotation("UnknownGrow") + .addTypeAnnotation("UnknownModifiability") + .addTypeAnnotation("UnknownReplace") + .addTypeAnnotation("UnknownShrink") + .addTypeAnnotation("Unmodifiable") } // Only define a groovyGradle task on the root project, for simplicity in setting the target pattern From bb890e45c43e001ba9268b4100173e656dae9bb9 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 17 Mar 2026 17:06:35 -0700 Subject: [PATCH 071/308] added stub files for javaparser --- .../checker/modifiability/grow/GrowChecker.java | 2 +- .../checker/modifiability/grow/javaparser.astub | 13 +++++++++++++ .../modifiability/replace/ReplaceChecker.java | 2 +- .../checker/modifiability/replace/javaparser.astub | 13 +++++++++++++ .../checker/modifiability/shrink/ShrinkChecker.java | 2 +- .../checker/modifiability/shrink/javaparser.astub | 13 +++++++++++++ 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 746935189773..5ca4b91355d8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -26,7 +26,7 @@ Map.Entry.class, Collections.class }) -@StubFiles({"ical4j.astub"}) +@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"Growable", "UnknownGrow"}) public class GrowChecker extends BaseTypeChecker { /** Creates a Grow checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub new file mode 100644 index 000000000000..cc63c87a8309 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub @@ -0,0 +1,13 @@ +import org.checkerframework.checker.modifiability.qual.Modifiable; + +package com.github.javaparser.ast.stmt; + +class BlockStmt { + public @Modifiable NodeList getStatements() {} +} + +package com.github.javaparser.ast; + +class CompilationUnit { + public @Modifiable NodeList getImports() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index 0dd106cf6ab9..f705ca9fbe54 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -26,7 +26,7 @@ Map.Entry.class, Collections.class }) -@StubFiles({"ical4j.astub"}) +@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"Replaceable", "UnknownReplace"}) public class ReplaceChecker extends BaseTypeChecker { /** Creates a Replace checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub new file mode 100644 index 000000000000..cc63c87a8309 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub @@ -0,0 +1,13 @@ +import org.checkerframework.checker.modifiability.qual.Modifiable; + +package com.github.javaparser.ast.stmt; + +class BlockStmt { + public @Modifiable NodeList getStatements() {} +} + +package com.github.javaparser.ast; + +class CompilationUnit { + public @Modifiable NodeList getImports() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index c22502a8fce3..f857ef7ea32d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -26,7 +26,7 @@ Map.Entry.class, Collections.class }) -@StubFiles({"ical4j.astub"}) +@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"Shrinkable", "UnknownShrink"}) public class ShrinkChecker extends BaseTypeChecker { /** Creates a Shrink checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub new file mode 100644 index 000000000000..cc63c87a8309 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub @@ -0,0 +1,13 @@ +import org.checkerframework.checker.modifiability.qual.Modifiable; + +package com.github.javaparser.ast.stmt; + +class BlockStmt { + public @Modifiable NodeList getStatements() {} +} + +package com.github.javaparser.ast; + +class CompilationUnit { + public @Modifiable NodeList getImports() {} +} From 94d1d0a0515babf62fe4a0f8680385232b0c03db Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 23 Mar 2026 15:00:46 -0700 Subject: [PATCH 072/308] change all collection to be unknownReplaceable. --- .../replace/ReplaceAnnotatedTypeFactory.java | 21 ++++++++++++++++--- .../tests/modifiability/PolyShrinkTest.java | 8 ++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index cde0ab0a3e74..e6ebaf999429 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -30,6 +30,9 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Set} type. */ private final TypeMirror setErasure; + /** The erased {@code java.util.Collection} type. */ + private final TypeMirror collectionErasure; + /** The erased {@code java.util.Queue} type. */ private final TypeMirror queueErasure; @@ -59,6 +62,8 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { // Cache type erasures. Types types = getProcessingEnv().getTypeUtils(); this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); + this.collectionErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Collection").asType()); this.queueErasure = types.erasure(getElementUtils().getTypeElement("java.util.Queue").asType()); this.linkedListErasure = types.erasure(getElementUtils().getTypeElement("java.util.LinkedList").asType()); @@ -94,6 +99,8 @@ protected TypeAnnotator createTypeAnnotator() { * Removes capabilities that cannot be supported by structural constraints of the collection type: * *

    + *
  • Collection itself (not subtypes): remove Replace capability → set Replace to + * {@code @UnknownReplace} *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to * {@code @UnknownReplace} *
  • Iterator: remove Replace capabilities @@ -114,8 +121,12 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { } TypeMirror underlyingType = type.getUnderlyingType(); + TypeMirror erasedUnderlyingType = types.erasure(underlyingType); - if (TypesUtils.isErasedSubtype(underlyingType, setErasure, types)) { + if (types.isSameType(erasedUnderlyingType, collectionErasure)) { + // Collection itself: Drop R bit + type.replaceAnnotation(UNKNOWN_REPLACE); + } else if (TypesUtils.isErasedSubtype(underlyingType, setErasure, types)) { // Set: Drop R bit type.replaceAnnotation(UNKNOWN_REPLACE); } else if (TypesUtils.isErasedSubtype(underlyingType, queueErasure, types) @@ -124,7 +135,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { type.replaceAnnotation(UNKNOWN_REPLACE); } else if (TypesUtils.isErasedSubtype(underlyingType, iteratorErasure, types) && !TypesUtils.isErasedSubtype(underlyingType, listIteratorErasure, types)) { - // Iterator: Drop G and R bits + // Iterator: Drop R bits type.replaceAnnotation(UNKNOWN_REPLACE); } return null; @@ -139,7 +150,11 @@ protected QualifierUpperBounds createQualifierUpperBounds() { @Override public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { - if (TypesUtils.isErasedSubtype(type, setErasure, types)) { + TypeMirror erasedType = types.erasure(type); + if (types.isSameType(erasedType, collectionErasure)) { + // Elements of a raw Collection are treated as @UnknownReplace. + return unknownReplace; + } else if (TypesUtils.isErasedSubtype(type, setErasure, types)) { // Elements of a set can never be replaced, so treat them as @UnknownReplace. Even if // they are annotation @Modifiable in a stubfile. return unknownReplace; diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java index f1191ee1217b..75a83ed11203 100644 --- a/checker/tests/modifiability/PolyShrinkTest.java +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -22,23 +22,19 @@ void testMapKeySet() { } void testMapValues() { - // Modifiable map: values() returns @Shrinkable. Cannot add or replace. + // Modifiable map: values() returns @Shrinkable. Cannot add. @Modifiable Map modMap = new java.util.HashMap<>(); @Shrinkable Collection modVals = modMap.values(); // :: error: [assignment] @Growable Collection modValsG = modMap.values(); // Cannot add - // :: error: [assignment] - @Replaceable Collection modValsR = modMap.values(); // Cannot replace - // Unmodifiable map: values() returns @Unmodifiable. Cannot shrink, add, or replace. + // Unmodifiable map: values() returns @Unmodifiable. Cannot shrink and add. @Unmodifiable Map unmodMap = Map.of("a", "b"); @Unmodifiable Collection unmodVals = unmodMap.values(); // :: error: [assignment] @Shrinkable Collection unmodValsS = unmodMap.values(); // Cannot shrink // :: error: [assignment] @Growable Collection unmodValsG = unmodMap.values(); // Cannot add - // :: error: [assignment] - @Replaceable Collection unmodValsR = unmodMap.values(); // Cannot replace } void testMapEntrySet() { From 299e5d37426a99b2ca20186afae0ab1952bec312 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 1 Apr 2026 16:12:23 -0700 Subject: [PATCH 073/308] added SetRefinementTest to simulate an issue i found with randoop: Inside constructor, the first modification call to a list doesn't get caught by modifiability checker --- .../modifiability/SetRefinementTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 checker/tests/modifiability/SetRefinementTest.java diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java new file mode 100644 index 000000000000..cd8bacd1f955 --- /dev/null +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -0,0 +1,36 @@ +import java.util.ArrayList; +import java.util.List; + +// Reproduces a dataflow refinement issue: calling set() on a field initialized with +// new ArrayList<>() invalidates the @Modifiable refinement because set() lacks +// @SideEffectsOnly("this"). After the set() call, the field reverts to its declared +// type (@UnknownModifiability), causing subsequent mutation calls to fail. +public class SetRefinementTest { + + private List items = new ArrayList<>(); + + /** Creates a new SetRefinementTest. */ + public SetRefinementTest() {} + + /** + * Creates a new SetRefinementTest. + * + * @param other list to copy from + */ + public SetRefinementTest(List other) { + this.items.addAll(other); + } + + public SetRefinementTest(List other, int dummy) { + this.items.set(0, other.get(0)); + // :: error: [method.invocation] + this.items.addAll(other); + } + + public SetRefinementTest(List other, boolean dummy) { + this.items.set(0, other.get(0)); + this.items.set(0, other.get(0)); + // :: error: [method.invocation] + this.items.addAll(other); + } +} From b7dbc737bbb963b0d3ddcc54f806e455228555bd Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 2 Apr 2026 12:24:14 -0700 Subject: [PATCH 074/308] refine SetRefinementTest --- checker/tests/modifiability/SetRefinementTest.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index cd8bacd1f955..1c23bdbbfa3f 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -18,17 +18,25 @@ public SetRefinementTest() {} * @param other list to copy from */ public SetRefinementTest(List other) { - this.items.addAll(other); + this.items.addAll(other); // this should not be allowed but checker is not checking this } public SetRefinementTest(List other, int dummy) { - this.items.set(0, other.get(0)); + this.items.set(0, other.get(0)); // this should not be allowed but checker is not checking this // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, boolean dummy) { + this.items.set(0, other.get(0)); // this should not be allowed but checker is not checking this + // :: error: [method.invocation] this.items.set(0, other.get(0)); + // :: error: [method.invocation] + this.items.addAll(other); + } + + public void updateItemsLikeConstructor(List other, int dummy) { + // :: error: [method.invocation] this.items.set(0, other.get(0)); // :: error: [method.invocation] this.items.addAll(other); From 206bdd5312f6baaf8ef06ad570132250c2789ebe Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 2 Apr 2026 15:49:03 -0700 Subject: [PATCH 075/308] updated setRefinementTest.java --- checker/tests/modifiability/SetRefinementTest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 1c23bdbbfa3f..6743116f2d2d 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -22,7 +22,9 @@ public SetRefinementTest(List other) { } public SetRefinementTest(List other, int dummy) { - this.items.set(0, other.get(0)); // this should not be allowed but checker is not checking this + // checker immediately sees that items' flow-type is @Modifiable, so this is ok + this.items.set(0, other.get(0)); + // since set is not side-effect-free, we lose the @Modifiable refinement // :: error: [method.invocation] this.items.addAll(other); } @@ -35,6 +37,13 @@ public SetRefinementTest(List other, boolean dummy) { this.items.addAll(other); } + public SetRefinementTest(List other, float dummy) { + this.items.get(0); // get is side-effect-free, after this line, items is still @Modifiable + this.items.set(0, other.get(0)); // ok + // :: error: [method.invocation] + this.items.addAll(other); + } + public void updateItemsLikeConstructor(List other, int dummy) { // :: error: [method.invocation] this.items.set(0, other.get(0)); From 6ab4b65cf67ce305e6e5e079ade8a7ad2e36fd7c Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 5 Apr 2026 18:21:52 -0700 Subject: [PATCH 076/308] updated build file to handle submodule --- build.gradle | 17 ++++++++--------- checker/build.gradle | 7 ++++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 7abed9ccecf1..4acede03d0a1 100644 --- a/build.gradle +++ b/build.gradle @@ -73,28 +73,27 @@ ext { javadocMemberLevel = JavadocMemberLevel.PROTECTED - // The local git repository, typically in the .git directory, but not for worktrees. + // Path to git hooks for this checkout (works for normal repos, worktrees, and submodules). // This value is always overwritten, but Gradle needs the variable to be initialized. - localRepo = ".git" + localHooksDir = ".git/hooks" } -tasks.register("setLocalRepo", Exec) { - commandLine("git", "worktree", "list") +tasks.register("setLocalHooksDir", Exec) { + commandLine("git", "rev-parse", "--git-path", "hooks") standardOutput = new ByteArrayOutputStream() doLast { - String worktreeList = standardOutput.toString() - localRepo = worktreeList.substring(0, worktreeList.indexOf(" ")) + "/.git" + localHooksDir = standardOutput.toString().trim() } } // No group so it does not show up in the output of `gradlew tasks` tasks.register("installGitHooks", Copy) { - dependsOn("setLocalRepo") - description = "Copies git hooks to .git directory" + dependsOn("setLocalHooksDir") + description = "Copies git hooks to the local git hooks directory" from(files("checker/bin-devel/git.post-merge", "checker/bin-devel/git.pre-commit")) rename("git\\.(.*)", "\$1") - into(localRepo + "/hooks") + into({ localHooksDir }) } subprojects { subProject -> if (subProject.name.equals("checker") diff --git a/checker/build.gradle b/checker/build.gradle index c9164435b1ea..3a9bdb61d0fc 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -86,8 +86,13 @@ dependencies { // https://docs.gradle.org/7.1/userguide/validation_problems.html#implicit_dependency for more // details about this problem. generateGitProperties.dependsOn(":installGitHooks") +def resolvedGitDir = providers.exec { + workingDir = rootProject.projectDir + commandLine("git", "rev-parse", "--absolute-git-dir") +}.standardOutput.asText.map { it.trim() } + gitProperties { - dotGitDirectory = project.rootProject.layout.projectDirectory.dir(".git") + dotGitDirectory = file(resolvedGitDir.get()) } jar { From 82404dfccea2a46bd7a4e344ade982fe61d0f0cf Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 6 Apr 2026 17:54:50 -0700 Subject: [PATCH 077/308] Don't let a field initializer affect the initial store for constructors --- checker/tests/modifiability/SetRefinementTest.java | 4 ++++ .../framework/type/GenericAnnotatedTypeFactory.java | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 6743116f2d2d..757aea2f0aa5 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -18,11 +18,13 @@ public SetRefinementTest() {} * @param other list to copy from */ public SetRefinementTest(List other) { + // :: error: [method.invocation] this.items.addAll(other); // this should not be allowed but checker is not checking this } public SetRefinementTest(List other, int dummy) { // checker immediately sees that items' flow-type is @Modifiable, so this is ok + // :: error: [method.invocation] this.items.set(0, other.get(0)); // since set is not side-effect-free, we lose the @Modifiable refinement // :: error: [method.invocation] @@ -30,6 +32,7 @@ public SetRefinementTest(List other, int dummy) { } public SetRefinementTest(List other, boolean dummy) { + // :: error: [method.invocation] this.items.set(0, other.get(0)); // this should not be allowed but checker is not checking this // :: error: [method.invocation] this.items.set(0, other.get(0)); @@ -39,6 +42,7 @@ public SetRefinementTest(List other, boolean dummy) { public SetRefinementTest(List other, float dummy) { this.items.get(0); // get is side-effect-free, after this line, items is still @Modifiable + // :: error: [method.invocation] this.items.set(0, other.get(0)); // ok // :: error: [method.invocation] this.items.addAll(other); diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java index b71530752bea..bf9cc123d5a0 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1371,6 +1371,7 @@ protected void performFlowAnalysisForClass(ClassTree classTree) { methods.add(method); break; case VARIABLE: + // A top-level variable is a field. VariableTree vt = (VariableTree) m; ExpressionTree initializer = vt.getInitializer(); AnnotatedTypeMirror declaredType = getAnnotatedTypeLhs(vt); @@ -1386,8 +1387,8 @@ protected void performFlowAnalysisForClass(ClassTree classTree) { new CFGStatement(vt, ct), fieldValues, null, - true, - true, + /* isInitializationCode= */ true, + /* updateInitializationStore= */ false, isStatic, capturedStore); postAnalyze(cfg); From 71c81e78a3e21e7e91f5ead9c9ec84c25d2eedc2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 9 Apr 2026 14:09:59 -0700 Subject: [PATCH 078/308] Add `@DoesNotUnrefineReceiver` annotation --- .../qual/DoesNotUnrefineReceiver.java | 23 ++++++++++++++++++ .../checker/lock/LockStore.java | 2 +- docs/CHANGELOG.md | 3 +++ .../framework/flow/CFAbstractStore.java | 2 +- .../framework/type/AnnotatedTypeFactory.java | 24 +++++++++++++++++++ .../javacutil/AnnotationProvider.java | 9 +++++++ .../javacutil/BasicAnnotationProvider.java | 20 ++++++++++++++++ 7 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java new file mode 100644 index 000000000000..4f99071a36c4 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java @@ -0,0 +1,23 @@ +package org.checkerframework.dataflow.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; + +/** + * At a call to a side-effecting method, the framework ordinarily discards all refined type + * information, since the side-effecting method might invalidate that information. This annotation + * indicates that, at a call to the annotated method, the receiver's type should not be unrefinde. + * That is, a call to the method does not affect the type qualifier (in the given hierarchy). + * + * @checker_framework.manual #type-refinement-purity Side effects, determinism, purity, and + * flow-sensitive analysis + */ +// @InheritedAnnotation cannot be written here, because "dataflow" project cannot depend on +// "framework" project. +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface DoesNotUnrefineReceiver {} diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java b/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java index 121cc454fbeb..763fa24cc222 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java @@ -172,7 +172,7 @@ public void updateForMethodCall( super.updateForMethodCall(n, atypeFactory, val); ExecutableElement method = n.getTarget().getMethod(); // The following behavior is similar to setting the sideEffectsUnrefineAliases field of - // Lockannotatedtypefactory, but it affects only the LockPosssiblyHeld type hierarchy (not + // LockAnnotatedTypeFactory, but it affects only the LockPosssiblyHeld type hierarchy (not // the @GuardedBy hierarchy), so it cannot use that logic. if (!atypeFactory.isSideEffectFree(method)) { // After the call to super.updateForMethodCall, only final fields are left in diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 02f88a04ae6d..39b15aeb9df4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,9 @@ ### Implementation details +New method annotation `DoesNotUnrefineReceiver`. +Interface `AnnotationProvider` has a new method `isDoesNotUnrefineReceiver()`. + ### Closed issues ## Version 4.0.0 (2026-04-07) 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 37cb1363b293..fbd160e8a05f 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -246,7 +246,7 @@ public void updateForMethodCall( } // update this value - if (sideEffectsUnrefineAliases) { + if (sideEffectsUnrefineAliases && !atypeFactory.isDoesNotUnrefineReceiver(method)) { thisValue = null; } diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 1a2d08d94220..9346c3b0ae5f 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -96,6 +96,7 @@ import org.checkerframework.common.wholeprograminference.WholeProgramInferenceJavaParserStorage; import org.checkerframework.common.wholeprograminference.WholeProgramInferenceJavaParserStorage.InferredDeclared; import org.checkerframework.common.wholeprograminference.WholeProgramInferenceScenesStorage; +import org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver; import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.framework.qual.AnnotatedFor; import org.checkerframework.framework.qual.EnsuresQualifier; @@ -218,6 +219,9 @@ public class AnnotatedTypeFactory implements AnnotationProvider { /** The AnnotatedFor.value argument/element. */ protected final ExecutableElement annotatedForValueElement; + /** The DoesNotUnrefineReceiver.value argument/element. */ + protected final ExecutableElement doesNotUnrefineReceiverValueElement; + /** The EnsuresQualifier.expression field/element. */ protected final ExecutableElement ensuresQualifierExpressionElement; @@ -680,6 +684,8 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { this.debugStubParser = checker.hasOption("stubDebug"); annotatedForValueElement = TreeUtils.getMethod(AnnotatedFor.class, "value", 0, processingEnv); + doesNotUnrefineReceiverValueElement = + TreeUtils.getMethod(DoesNotUnrefineReceiver.class, "value", 0, processingEnv); ensuresQualifierExpressionElement = TreeUtils.getMethod(EnsuresQualifier.class, "expression", 0, processingEnv); ensuresQualifierListValueElement = @@ -5875,6 +5881,24 @@ public boolean isDeterministic(ExecutableElement methodElement) { return false; } + @Override + public boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement) { + for (AnnotationMirror am : getDeclAnnotations(methodElement)) { + if (areSameByClass(am, org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver.class)) { + List typeSystems = + AnnotationUtils.getElementValueArray( + am, doesNotUnrefineReceiverValueElement, String.class); + for (String prefix : checker.getSuppressWarningsPrefixes()) { + if (typeSystems.contains(prefix)) { + return true; + } + } + return false; + } + } + return false; + } + /** * Output a message about {@link #getAnnotatedType}, if logging is on. * diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java index e71ba812dd0b..876d1a01ffc5 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java @@ -56,4 +56,13 @@ public interface AnnotationProvider { * @return true if multiple calls to the method (with the same arguments) return the same value */ boolean isDeterministic(ExecutableElement methodElement); + + /** + * Returns true if the given method may have side effects but does not does not unrefine its + * receiver. + * + * @param methodElement a method + * @return true if the method does not unrefine its receiver + */ + boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement); } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java index a160fd8a800c..390627fc3af6 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java @@ -83,4 +83,24 @@ public boolean isDeterministic(ExecutableElement methodElement) { return false; } + + /** + * {@inheritDoc} + * + *

    This implementation returns true if the {@code @DoesNotUnrefineReceiver} annotation is + * present on the given method, with an empty "values" element. + */ + @Override + public boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement) { + for (AnnotationMirror am : methodElement.getAnnotationMirrors()) { + if (AnnotationUtils.areSameByName( + am, "org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver")) { + @SuppressWarnings("deprecation") // calls from the framework are permitted + List typeSystems = + AnnotationUtils.getElementValueArray(am, "value", String.class, true); + return typeSystems.isEmpty(); + } + } + return false; + } } From b49b57106d6d07779d54d38279e4254f553e9a5d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 9 Apr 2026 18:15:37 -0700 Subject: [PATCH 079/308] Add field --- .../dataflow/qual/DoesNotUnrefineReceiver.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java index 4f99071a36c4..36c2269bbccc 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java @@ -20,4 +20,10 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -public @interface DoesNotUnrefineReceiver {} +public @interface DoesNotUnrefineReceiver { + /** + * The name of the checker that this annotation affects. For example, "modifiability" or + * "nullness". + */ + String[] value = {}; +} From 417bc1ad5293563b474d56d50886c006959d9d02 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 03:06:51 -0700 Subject: [PATCH 080/308] Fix element definition --- .../dataflow/qual/DoesNotUnrefineReceiver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java index 36c2269bbccc..22bef1387e67 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java @@ -23,7 +23,7 @@ public @interface DoesNotUnrefineReceiver { /** * The name of the checker that this annotation affects. For example, "modifiability" or - * "nullness". + * "nullness". Use the empty array to affect all checkers. */ - String[] value = {}; + String[] value(); } From ead1266e4d3af59ff0a2be37e4dad846a7b6b135 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 04:49:47 -0700 Subject: [PATCH 081/308] Checkpoint --- .../checker/modifiability/grow/GrowChecker.java | 2 +- .../checker/modifiability/replace/ReplaceChecker.java | 2 +- .../checker/modifiability/shrink/ShrinkChecker.java | 2 +- checker/tests/modifiability/SetRefinementTest.java | 8 -------- .../framework/type/GenericAnnotatedTypeFactory.java | 2 +- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 5ca4b91355d8..e3f9caa9937a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -27,7 +27,7 @@ Collections.class }) @StubFiles({"ical4j.astub", "javaparser.astub"}) -@SuppressWarningsPrefix({"Growable", "UnknownGrow"}) +@SuppressWarningsPrefix({"growable", "modifiability"}) public class GrowChecker extends BaseTypeChecker { /** Creates a Grow checker. */ public GrowChecker() {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index f705ca9fbe54..0eb251e2e07f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -27,7 +27,7 @@ Collections.class }) @StubFiles({"ical4j.astub", "javaparser.astub"}) -@SuppressWarningsPrefix({"Replaceable", "UnknownReplace"}) +@SuppressWarningsPrefix({"replaceable", "modifiability"}) public class ReplaceChecker extends BaseTypeChecker { /** Creates a Replace checker. */ public ReplaceChecker() {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index f857ef7ea32d..8fbce8491a1e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -27,7 +27,7 @@ Collections.class }) @StubFiles({"ical4j.astub", "javaparser.astub"}) -@SuppressWarningsPrefix({"Shrinkable", "UnknownShrink"}) +@SuppressWarningsPrefix({"shrinkable", "modifiability"}) public class ShrinkChecker extends BaseTypeChecker { /** Creates a Shrink checker. */ public ShrinkChecker() {} diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 757aea2f0aa5..34f82bf2ae58 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -18,33 +18,25 @@ public SetRefinementTest() {} * @param other list to copy from */ public SetRefinementTest(List other) { - // :: error: [method.invocation] this.items.addAll(other); // this should not be allowed but checker is not checking this } public SetRefinementTest(List other, int dummy) { // checker immediately sees that items' flow-type is @Modifiable, so this is ok - // :: error: [method.invocation] this.items.set(0, other.get(0)); // since set is not side-effect-free, we lose the @Modifiable refinement - // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, boolean dummy) { - // :: error: [method.invocation] this.items.set(0, other.get(0)); // this should not be allowed but checker is not checking this - // :: error: [method.invocation] this.items.set(0, other.get(0)); - // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, float dummy) { this.items.get(0); // get is side-effect-free, after this line, items is still @Modifiable - // :: error: [method.invocation] this.items.set(0, other.get(0)); // ok - // :: error: [method.invocation] this.items.addAll(other); } diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java index 6681413c2335..06a9ed53344b 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1389,7 +1389,7 @@ protected void performFlowAnalysisForClass(ClassTree classTree) { fieldValues, null, /* isInitializationCode= */ true, - /* updateInitializationStore= */ false, + /* updateInitializationStore= */ true, isStatic, capturedStore); postAnalyze(cfg); From 38cab476244dcc1f7899a7592a19fb3f173db76f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 11:27:17 -0700 Subject: [PATCH 082/308] Improve Javadoc style --- .../org/checkerframework/dataflow/analysis/TransferInput.java | 4 +--- .../org/checkerframework/framework/test/TypecheckResult.java | 4 ++-- .../framework/test/diagnostics/TestDiagnosticUtils.java | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java index b84bfc27da84..5a3f42f0f2c0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java @@ -29,9 +29,7 @@ public class TransferInput, S extends Store> imple * The regular result store (or {@code null} if none is present, because {@link #thenStore} and * {@link #elseStore} are set). The following invariant is maintained: * - *

    
    -   * store == null ⇔ thenStore != null && elseStore != null
    -   * 
    + *
    {@code store == null  ==>  thenStore != null && elseStore != null}
    */ protected final @Nullable S store; diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java index aec75670d2c8..44543b31aa67 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java @@ -37,8 +37,8 @@ public class TypecheckResult { /** * Creates a TypecheckResult. * - * @param configuration The test configuration. - * @param compilationResult The compilation result. + * @param configuration the test configuration. + * @param compilationResult the compilation result * @param expectedDiagnostics the expected diagnostics * @param missingDiagnostics the diagnostics that were expected but were not issued * @param unexpectedDiagnostics the diagnostics that were issued but were not expected diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java index 77026bbae456..a517dbbfc4c4 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java @@ -297,7 +297,7 @@ private static boolean retainAllLines(String message) { /** * Given a category string that may be prepended with "fixable-", return the category enum that - * corresponds with the category and whether or not it is a isFixable error + * corresponds with the category and whether or not it is a isFixable error. * * @param category a category string that may be prepended with "fixable-" * @return a pair of the category and whether it was prepended with "fixable-" From 6cdc844edb524c8e7e384cbed48007a108233ac5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 12:04:06 -0700 Subject: [PATCH 083/308] Improve Javadoc style --- .../java/org/checkerframework/afu/annotator/find/Insertion.java | 2 +- .../checkerframework/afu/annotator/scanner/TreePathUtil.java | 2 +- buildSrc/src/main/java/CloneOrUpdateRelatedTask.java | 2 +- buildSrc/src/main/java/CloneOrUpdateTask.java | 2 +- .../org/checkerframework/framework/test/TypecheckResult.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java index a9e9b8722d39..0ade2a5f6abc 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java @@ -258,7 +258,7 @@ public static String collectionToString(Collection list) { * * @param s the string representation of an annotation * @return given {@code @com.foo.bar(baz)} it returns a pair of {@code com.foo} and - * {@code @bar(baz)}. + * {@code @bar(baz)} */ public static IPair removePackage(String s) { int nameEnd = s.indexOf('('); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/scanner/TreePathUtil.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/scanner/TreePathUtil.java index 5457d687dbea..317ef41471c9 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/scanner/TreePathUtil.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/scanner/TreePathUtil.java @@ -275,7 +275,7 @@ public static int getEndPosition(Tree tree, CompilationUnitTree unit) { /** * Returns the {@link MethodHandle} for retrieving the end position of a {@link JCTree}. * - * @return the {@link MethodHandle} for retrieving the end position of a {@link JCTree}. + * @return the {@link MethodHandle} for retrieving the end position of a {@link JCTree} */ private static MethodHandle getEndPosMethodHandle() { MethodHandles.Lookup lookup = MethodHandles.lookup(); diff --git a/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java b/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java index 2e51a00b8c28..0459fabe2717 100644 --- a/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java +++ b/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java @@ -36,7 +36,7 @@ public abstract class CloneOrUpdateRelatedTask extends GitTask { */ private static final String DEFAULT_ORG = "typetools"; - /** The branch to use to clone the related repository if a matching branch is not found */ + /** The branch to use to clone the related repository if a matching branch is not found. */ private static final String DEFAULT_BRANCH = "master"; /** diff --git a/buildSrc/src/main/java/CloneOrUpdateTask.java b/buildSrc/src/main/java/CloneOrUpdateTask.java index 4aee60bf7d9d..79ca0d23be9e 100644 --- a/buildSrc/src/main/java/CloneOrUpdateTask.java +++ b/buildSrc/src/main/java/CloneOrUpdateTask.java @@ -13,7 +13,7 @@ public abstract class CloneOrUpdateTask extends GitTask { /** * The URL to clone or update. * - * @return the URL to clone or update. + * @return the URL to clone or update */ @Input public abstract Property getUrl(); diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java index 44543b31aa67..23f1b7b26d96 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java @@ -37,7 +37,7 @@ public class TypecheckResult { /** * Creates a TypecheckResult. * - * @param configuration the test configuration. + * @param configuration the test configuration * @param compilationResult the compilation result * @param expectedDiagnostics the expected diagnostics * @param missingDiagnostics the diagnostics that were expected but were not issued From c3955a404dbc4308aad8df9efc71a17f04fc8689 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 13:00:19 -0700 Subject: [PATCH 084/308] Improve Javadoc style --- buildSrc/src/main/java/CloneOrUpdateRelatedTask.java | 2 +- .../org/checkerframework/javacutil/TreeUtilsAfterJava17.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java b/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java index 0459fabe2717..cd9091fe8217 100644 --- a/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java +++ b/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java @@ -50,7 +50,7 @@ public abstract class CloneOrUpdateRelatedTask extends GitTask { /** * Creates a new CloneOrUpdateRelatedTask. * - * @param execOperations Used to run exec commands + * @param execOperations used to run exec commands */ @Inject public CloneOrUpdateRelatedTask(ExecOperations execOperations) { diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java index d0d46343803b..ee745f08c3f0 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java @@ -100,7 +100,7 @@ public static List getLabels(CaseTree caseTree) { * might contain a single pattern tree. * * @param caseTree the case expression to get the labels from - * @param useDefaultCaseLabelTree whether the result should contain a {@code + * @param useDefaultCaseLabelTree true if the result should contain a {@code * DefaultCaseLabelTree}. * @return the list of case labels in the case */ From 02505662e01656928197fd97adc01f7df55b45dd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 15:10:21 -0700 Subject: [PATCH 085/308] Fix bi-implication --- .../org/checkerframework/dataflow/analysis/TransferInput.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java index 5a3f42f0f2c0..91290e80d787 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java @@ -29,7 +29,7 @@ public class TransferInput, S extends Store> imple * The regular result store (or {@code null} if none is present, because {@link #thenStore} and * {@link #elseStore} are set). The following invariant is maintained: * - *
    {@code store == null  ==>  thenStore != null && elseStore != null}
    + *
    {@code store == null  <==>  thenStore != null && elseStore != null}
    */ protected final @Nullable S store; From faabc74d90159e407b1e50d5b1ddfa08808edda1 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 15:11:04 -0700 Subject: [PATCH 086/308] Grammar --- .../framework/test/diagnostics/TestDiagnosticUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java index a517dbbfc4c4..bcb5a4d25890 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java @@ -297,7 +297,7 @@ private static boolean retainAllLines(String message) { /** * Given a category string that may be prepended with "fixable-", return the category enum that - * corresponds with the category and whether or not it is a isFixable error. + * corresponds with the category and whether or not it is an isFixable error. * * @param category a category string that may be prepended with "fixable-" * @return a pair of the category and whether it was prepended with "fixable-" From d173f9f222a17cda0498fa560d611da28598e4b7 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 16:17:06 -0700 Subject: [PATCH 087/308] Checkpoint --- .../main/java/CloneOrUpdateRelatedTask.java | 4 +- .../nullness/qual/EnsuresNonNullIf.java | 2 +- .../dataflow/analysis/TransferInput.java | 4 +- .../dataflow/expression/JavaExpression.java | 8 +-- docs/CHANGELOG.md | 2 +- .../framework/test/TypecheckResult.java | 4 +- .../test/diagnostics/TestDiagnosticUtils.java | 2 +- .../framework/flow/CFAbstractStore.java | 67 ++++++++++++++++--- .../framework/type/AnnotatedTypeFactory.java | 12 +++- .../javacutil/AnnotationProvider.java | 2 +- .../javacutil/BasicAnnotationProvider.java | 2 +- .../javacutil/TreeUtilsAfterJava17.java | 4 +- 12 files changed, 87 insertions(+), 26 deletions(-) diff --git a/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java b/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java index 2e51a00b8c28..cd9091fe8217 100644 --- a/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java +++ b/buildSrc/src/main/java/CloneOrUpdateRelatedTask.java @@ -36,7 +36,7 @@ public abstract class CloneOrUpdateRelatedTask extends GitTask { */ private static final String DEFAULT_ORG = "typetools"; - /** The branch to use to clone the related repository if a matching branch is not found */ + /** The branch to use to clone the related repository if a matching branch is not found. */ private static final String DEFAULT_BRANCH = "master"; /** @@ -50,7 +50,7 @@ public abstract class CloneOrUpdateRelatedTask extends GitTask { /** * Creates a new CloneOrUpdateRelatedTask. * - * @param execOperations Used to run exec commands + * @param execOperations used to run exec commands */ @Inject public CloneOrUpdateRelatedTask(ExecOperations execOperations) { diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java index d74189c154ce..5d0fcf215b95 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java @@ -84,7 +84,7 @@ /** * Returns Java expression(s) that are non-null after the method returns the given result. * - * @return Java expression(s) that are non-null after the method returns the given result + * @return the Java expression(s) that are non-null after the method returns the given result * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions */ String[] expression(); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java index b84bfc27da84..bbfebc81523a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java @@ -29,9 +29,9 @@ public class TransferInput, S extends Store> imple * The regular result store (or {@code null} if none is present, because {@link #thenStore} and * {@link #elseStore} are set). The following invariant is maintained: * - *
    
    +   * 
    {@code
        * store == null ⇔ thenStore != null && elseStore != null
    -   * 
    + * }
    */ protected final @Nullable S store; 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 3dd9b12598ed..2f68b5caf527 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -319,12 +319,12 @@ public static JavaExpression fromNodeFieldAccess(FieldAccessNode node) { } /** - * Returns the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. The - * result may contain {@link Unknown} as receiver. + * Returns the internal representation (as an {@link ArrayAccess}) of an {@link ArrayAccessNode}. + * The result may contain {@link Unknown} as receiver. * * @param node the ArrayAccessNode to convert to a JavaExpression - * @return the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. Can - * contain {@link Unknown} as receiver. + * @return the internal representation (as an {@link ArrayAccess}) of a {@link ArrayAccessNode}. + * Can contain {@link Unknown} as receiver. */ public static ArrayAccess fromArrayAccess(ArrayAccessNode node) { JavaExpression array = fromNode(node.getArray()); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5a2c90c61eb0..87ef10b08bb1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,7 +12,7 @@ New `wpi2.sh` script does whole-program inference, but requires buildfile edits. ### Implementation details New method annotation `DoesNotUnrefineReceiver`. -Interface `AnnotationProvider` has a new method `isDoesNotUnrefineReceiver()`. +Interface `AnnotationProvider` has a new method `hasDoesNotUnrefineReceiver()`. ### Closed issues diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java index aec75670d2c8..dfc96ba4b627 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckResult.java @@ -37,8 +37,8 @@ public class TypecheckResult { /** * Creates a TypecheckResult. * - * @param configuration The test configuration. - * @param compilationResult The compilation result. + * @param configuration the test configuration. + * @param compilationResult the compilation result. * @param expectedDiagnostics the expected diagnostics * @param missingDiagnostics the diagnostics that were expected but were not issued * @param unexpectedDiagnostics the diagnostics that were issued but were not expected diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java index 77026bbae456..a517dbbfc4c4 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java @@ -297,7 +297,7 @@ private static boolean retainAllLines(String message) { /** * Given a category string that may be prepended with "fixable-", return the category enum that - * corresponds with the category and whether or not it is a isFixable error + * corresponds with the category and whether or not it is a isFixable error. * * @param category a category string that may be prepended with "fixable-" * @return a pair of the category and whether it was prepended with "fixable-" 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 fbd160e8a05f..d79210db16be 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -10,6 +10,7 @@ import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BinaryOperator; +import java.util.function.Predicate; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; @@ -236,26 +237,67 @@ public void updateForMethodCall( || atypeFactory.isSideEffectFree(method)); if (hasSideEffect) { + // TODO: For each of these, if it happens to be the receiver, don't unrefine it.x + boolean sideEffectsUnrefineAliases = gatypeFactory.sideEffectsUnrefineAliases; + Node receiver = methodInvocationNode.getTarget().getReceiver(); + System.out.printf("receiver = %s [%s]%n", receiver, receiver.getClass()); + boolean hasDoesNotUnrefineReceiver = atypeFactory.hasDoesNotUnrefineReceiver(method); + System.out.printf( + "hasDoesNotUnrefineReceiver(%s) = %s%n", method, hasDoesNotUnrefineReceiver); + + // Runnable shouldRemove + // = (JavaExpression je) -> !(hasDoesNotUnrefineReceiver && fa == receiver) + // && fa.isModifiableByOtherCode() - // update local variables // TODO: Also remove if any element/argument to the annotation is not // isUnmodifiableByOtherCode. Example: @KeyFor("valueThatCanBeMutated"). + + // Update local variables. if (sideEffectsUnrefineAliases) { localVariableValues.entrySet().removeIf(e -> e.getKey().isModifiableByOtherCode()); } - // update this value - if (sideEffectsUnrefineAliases && !atypeFactory.isDoesNotUnrefineReceiver(method)) { + // Update this value. + if (sideEffectsUnrefineAliases) { thisValue = null; } - // update field values + // Update field values. + System.out.printf("sideEffectsUnrefineAliases = %s%n", sideEffectsUnrefineAliases); + Predicate doNotUnrefine; + System.out.printf( + "hasDoesNotUnrefineReceiver=%s, receiver instanceof FieldAccessNode=%s%n", + hasDoesNotUnrefineReceiver, receiver instanceof FieldAccessNode); + if (hasDoesNotUnrefineReceiver && receiver instanceof FieldAccessNode receiverFan) { + doNotUnrefine = + (FieldAccess fa) -> { + System.out.printf( + " doNotUnrefine: receiverFan=%s [FieldAccessNode] %n", receiverFan); + JavaExpression receiverJe = JavaExpression.fromNodeFieldAccess(receiverFan); + System.out.printf( + " doNotUnrefine: receiverJe=%s [%s]%n", + receiverJe, receiverJe.getClass().getSimpleName()); + System.out.printf(" doNotUnrefine: fa=%s [FieldAccess]%n", fa); + return receiverJe + == System.out.printf(" doNotUnrefine: returning hard-coded false%n"); + return false; + }; // TODO + } else { + doNotUnrefine = (FieldAccess fa) -> false; + } if (sideEffectsUnrefineAliases) { - fieldValues.entrySet().removeIf(e -> e.getKey().isModifiableByOtherCode()); + fieldValues + .entrySet() + .removeIf( + (Map.Entry e) -> { + FieldAccess fa = e.getKey(); + System.out.printf(" removeif: fa = %s%n", fa); + return fa.isModifiableByOtherCode() && !doNotUnrefine.test(fa); + }); } else { // Case 2 (unassignable fields) and case 3 (monotonic fields) - updateFieldValuesForMethodCall(gatypeFactory); + updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefine); } // Update array values. @@ -358,15 +400,24 @@ protected V newMonotonicFieldValueAfterMethodCall( * fields that have a monotonic annotation. * * @param atypeFactory AnnotatedTypeFactory of the associated checker + * @param doNotUnrefine if true, */ private void updateFieldValuesForMethodCall( - GenericAnnotatedTypeFactory atypeFactory) { + GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefine) { + System.out.printf("entering updateFieldValuesForMethodCall%n"); Map newFieldValues = new HashMap<>(MapsP.mapCapacity(fieldValues)); for (Map.Entry e : fieldValues.entrySet()) { FieldAccess fieldAccess = e.getKey(); V previousValue = e.getValue(); - V newValue = newFieldValueAfterMethodCall(fieldAccess, atypeFactory, previousValue); + V newValue; + boolean doNotUnrefineResult = doNotUnrefine.test(fieldAccess); + System.out.printf(" doNotUnrefineResult(%s) => %s%n", fieldAccess, doNotUnrefineResult); + if (doNotUnrefineResult) { + newValue = previousValue; + } else { + newValue = newFieldValueAfterMethodCall(fieldAccess, atypeFactory, previousValue); + } if (newValue != null) { // Keep information for all hierarchies where we had a monotonic annotation. newFieldValues.put(fieldAccess, newValue); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 9346c3b0ae5f..10ebc7b80192 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -4062,6 +4062,13 @@ public boolean shouldWarnIfStubRedundantWithBytecode() { * @return all of the declaration annotations on this element, written in stub files, or inherited */ public AnnotationMirrorSet getDeclAnnotations(Element elt) { + boolean debug = false; + // debug = elt instanceof ExecutableElement; + if (debug) { + System.out.printf( + "Entering getDeclAnnotations(%s.%s)%n", + ((ExecutableElement) elt).getEnclosingElement().getSimpleName(), elt); + } AnnotationMirrorSet cachedValue = cacheDeclAnnos.get(elt); if (cachedValue != null) { // Found in cache, return result. @@ -4073,6 +4080,9 @@ public AnnotationMirrorSet getDeclAnnotations(Element elt) { // This includes annotations inherited from superclasses, but not superinterfaces or // overridden methods. List fromEle = elements.getAllAnnotationMirrors(elt); + if (debug) { + System.out.printf(" fromEle = %s%n", fromEle); + } for (AnnotationMirror annotation : fromEle) { try { results.add(annotation); @@ -5882,7 +5892,7 @@ public boolean isDeterministic(ExecutableElement methodElement) { } @Override - public boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement) { + public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { for (AnnotationMirror am : getDeclAnnotations(methodElement)) { if (areSameByClass(am, org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver.class)) { List typeSystems = diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java index 876d1a01ffc5..1ccb83f9ea31 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java @@ -64,5 +64,5 @@ public interface AnnotationProvider { * @param methodElement a method * @return true if the method does not unrefine its receiver */ - boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement); + boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement); } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java index 390627fc3af6..45e88acc8ecb 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java @@ -91,7 +91,7 @@ public boolean isDeterministic(ExecutableElement methodElement) { * present on the given method, with an empty "values" element. */ @Override - public boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement) { + public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { for (AnnotationMirror am : methodElement.getAnnotationMirrors()) { if (AnnotationUtils.areSameByName( am, "org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver")) { diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java index d0d46343803b..b2d9966ab7d0 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava17.java @@ -100,8 +100,8 @@ public static List getLabels(CaseTree caseTree) { * might contain a single pattern tree. * * @param caseTree the case expression to get the labels from - * @param useDefaultCaseLabelTree whether the result should contain a {@code - * DefaultCaseLabelTree}. + * @param useDefaultCaseLabelTree true if the result should contain a {@code + * DefaultCaseLabelTree} * @return the list of case labels in the case */ private static List getLabels( From c9aeff732b6ae10e215c64f5a047460734ab54b9 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 16:24:59 -0700 Subject: [PATCH 088/308] Undo partial work --- .../org/checkerframework/framework/flow/CFAbstractStore.java | 5 ++--- 1 file changed, 2 insertions(+), 3 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 b3a40fa7a0a0..a486a89dfadf 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -279,10 +279,9 @@ public void updateForMethodCall( " doNotUnrefine: receiverJe=%s [%s]%n", receiverJe, receiverJe.getClass().getSimpleName()); System.out.printf(" doNotUnrefine: fa=%s [FieldAccess]%n", fa); - return receiverJe - == System.out.printf(" doNotUnrefine: returning hard-coded false%n"); + // TODO: Adjust this test. return false; - }; // TODO + }; } else { doNotUnrefine = (FieldAccess fa) -> false; } From ecb45570592311ba56f7cd8dcc533f4ff383ba45 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 16:35:22 -0700 Subject: [PATCH 089/308] Fix test --- .../org/checkerframework/framework/flow/CFAbstractStore.java | 3 +-- 1 file changed, 1 insertion(+), 2 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 a486a89dfadf..2c87405b7120 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -279,8 +279,7 @@ public void updateForMethodCall( " doNotUnrefine: receiverJe=%s [%s]%n", receiverJe, receiverJe.getClass().getSimpleName()); System.out.printf(" doNotUnrefine: fa=%s [FieldAccess]%n", fa); - // TODO: Adjust this test. - return false; + return fa.equals(receiverJe); }; } else { doNotUnrefine = (FieldAccess fa) -> false; From 4587af0c17be4977f56b1334505fd3663f61d0f7 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 16:38:37 -0700 Subject: [PATCH 090/308] Rename and document --- .../framework/flow/CFAbstractStore.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 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 2c87405b7120..4aa1493a07be 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -265,24 +265,24 @@ public void updateForMethodCall( // Update field values. System.out.printf("sideEffectsUnrefineAliases = %s%n", sideEffectsUnrefineAliases); - Predicate doNotUnrefine; + Predicate doNotUnrefineField; System.out.printf( "hasDoesNotUnrefineReceiver=%s, receiver instanceof FieldAccessNode=%s%n", hasDoesNotUnrefineReceiver, receiver instanceof FieldAccessNode); if (hasDoesNotUnrefineReceiver && receiver instanceof FieldAccessNode receiverFan) { - doNotUnrefine = + doNotUnrefineField = (FieldAccess fa) -> { System.out.printf( - " doNotUnrefine: receiverFan=%s [FieldAccessNode] %n", receiverFan); + " doNotUnrefineField: receiverFan=%s [FieldAccessNode] %n", receiverFan); JavaExpression receiverJe = JavaExpression.fromNodeFieldAccess(receiverFan); System.out.printf( - " doNotUnrefine: receiverJe=%s [%s]%n", + " doNotUnrefineField: receiverJe=%s [%s]%n", receiverJe, receiverJe.getClass().getSimpleName()); - System.out.printf(" doNotUnrefine: fa=%s [FieldAccess]%n", fa); + System.out.printf(" doNotUnrefineField: fa=%s [FieldAccess]%n", fa); return fa.equals(receiverJe); }; } else { - doNotUnrefine = (FieldAccess fa) -> false; + doNotUnrefineField = (FieldAccess fa) -> false; } if (sideEffectsUnrefineAliases) { fieldValues @@ -291,11 +291,11 @@ public void updateForMethodCall( (Map.Entry e) -> { FieldAccess fa = e.getKey(); System.out.printf(" removeif: fa = %s%n", fa); - return fa.isModifiableByOtherCode() && !doNotUnrefine.test(fa); + return fa.isModifiableByOtherCode() && !doNotUnrefineField.test(fa); }); } else { // Case 2 (unassignable fields) and case 3 (monotonic fields) - updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefine); + updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefineField); } // Update array values. @@ -398,10 +398,12 @@ protected V newMonotonicFieldValueAfterMethodCall( * fields that have a monotonic annotation. * * @param atypeFactory AnnotatedTypeFactory of the associated checker - * @param doNotUnrefine if true, + * @param doNotUnrefineField if true of a field access, don't unrefine it. This predicate + * indicates exceptions: fields that is not updated by this method. */ private void updateFieldValuesForMethodCall( - GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefine) { + GenericAnnotatedTypeFactory atypeFactory, + Predicate doNotUnrefineField) { System.out.printf("entering updateFieldValuesForMethodCall%n"); Map newFieldValues = new HashMap<>(MapsP.mapCapacity(fieldValues)); for (Map.Entry e : fieldValues.entrySet()) { @@ -409,9 +411,10 @@ private void updateFieldValuesForMethodCall( V previousValue = e.getValue(); V newValue; - boolean doNotUnrefineResult = doNotUnrefine.test(fieldAccess); - System.out.printf(" doNotUnrefineResult(%s) => %s%n", fieldAccess, doNotUnrefineResult); - if (doNotUnrefineResult) { + boolean doNotUnrefineFieldResult = doNotUnrefineField.test(fieldAccess); + System.out.printf( + " doNotUnrefineFieldResult(%s) => %s%n", fieldAccess, doNotUnrefineFieldResult); + if (doNotUnrefineFieldResult) { newValue = previousValue; } else { newValue = newFieldValueAfterMethodCall(fieldAccess, atypeFactory, previousValue); From 71475aef52084d0e55af5aab797b97faeb502ac9 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 17:44:32 -0700 Subject: [PATCH 091/308] Handle other types of expressions --- .../framework/flow/CFAbstractStore.java | 65 ++++++++----------- 1 file changed, 28 insertions(+), 37 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 4aa1493a07be..7edc98ce54bc 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -237,8 +237,6 @@ public void updateForMethodCall( || atypeFactory.isSideEffectFree(method)); if (hasSideEffect) { - // TODO: For each of these, if it happens to be the receiver, don't unrefine it.x - boolean sideEffectsUnrefineAliases = gatypeFactory.sideEffectsUnrefineAliases; Node receiver = methodInvocationNode.getTarget().getReceiver(); System.out.printf("receiver = %s [%s]%n", receiver, receiver.getClass()); @@ -246,63 +244,61 @@ public void updateForMethodCall( System.out.printf( "hasDoesNotUnrefineReceiver(%s) = %s%n", method, hasDoesNotUnrefineReceiver); - // Runnable shouldRemove - // = (JavaExpression je) -> !(hasDoesNotUnrefineReceiver && fa == receiver) - // && fa.isModifiableByOtherCode() - // TODO: Also remove if any element/argument to the annotation is not // isUnmodifiableByOtherCode. Example: @KeyFor("valueThatCanBeMutated"). + // If @DoesNotUnrefineReceiver is present, compute the receiver as a JavaExpression so + // that it can be exempted from unrefinement in all expression categories below. + @Nullable JavaExpression receiverJe = + hasDoesNotUnrefineReceiver ? JavaExpression.fromNode(receiver) : null; + // Returns true if the expression should NOT be unrefined (because the method is + // annotated @DoesNotUnrefineReceiver and the expression is the receiver). + Predicate doNotUnrefine = + receiverJe != null ? je -> je.equals(receiverJe) : je -> false; + // Update local variables. if (sideEffectsUnrefineAliases) { - localVariableValues.entrySet().removeIf(e -> e.getKey().isModifiableByOtherCode()); + localVariableValues + .entrySet() + .removeIf( + e -> { + LocalVariable lv = e.getKey(); + return lv.isModifiableByOtherCode() && !doNotUnrefine.test(lv); + }); } // Update this value. - if (sideEffectsUnrefineAliases) { + if (sideEffectsUnrefineAliases && !(receiverJe instanceof ThisReference)) { thisValue = null; } // Update field values. System.out.printf("sideEffectsUnrefineAliases = %s%n", sideEffectsUnrefineAliases); - Predicate doNotUnrefineField; - System.out.printf( - "hasDoesNotUnrefineReceiver=%s, receiver instanceof FieldAccessNode=%s%n", - hasDoesNotUnrefineReceiver, receiver instanceof FieldAccessNode); - if (hasDoesNotUnrefineReceiver && receiver instanceof FieldAccessNode receiverFan) { - doNotUnrefineField = - (FieldAccess fa) -> { - System.out.printf( - " doNotUnrefineField: receiverFan=%s [FieldAccessNode] %n", receiverFan); - JavaExpression receiverJe = JavaExpression.fromNodeFieldAccess(receiverFan); - System.out.printf( - " doNotUnrefineField: receiverJe=%s [%s]%n", - receiverJe, receiverJe.getClass().getSimpleName()); - System.out.printf(" doNotUnrefineField: fa=%s [FieldAccess]%n", fa); - return fa.equals(receiverJe); - }; - } else { - doNotUnrefineField = (FieldAccess fa) -> false; - } + Predicate doNotUnrefineField = fa -> doNotUnrefine.test(fa); if (sideEffectsUnrefineAliases) { fieldValues .entrySet() .removeIf( (Map.Entry e) -> { FieldAccess fa = e.getKey(); - System.out.printf(" removeif: fa = %s%n", fa); - return fa.isModifiableByOtherCode() && !doNotUnrefineField.test(fa); + return fa.isModifiableByOtherCode() && !doNotUnrefine.test(fa); }); } else { - // Case 2 (unassignable fields) and case 3 (monotonic fields) + // Case 2 (unassignable fields) and case 3 (monotonic fields). updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefineField); } // Update array values. - arrayValues.clear(); + arrayValues.entrySet().removeIf(e -> !doNotUnrefine.test(e.getKey())); // Update information about method calls. - updateMethodCallValues(); + methodCallExpressions + .entrySet() + .removeIf( + e -> { + MethodCall mc = e.getKey(); + return mc.isModifiableByOtherCode() && !doNotUnrefine.test(mc); + }); } // Store information about method calls if possible. @@ -310,11 +306,6 @@ public void updateForMethodCall( replaceValue(methodCall, val); } - /** Update information about method calls. */ - private void updateMethodCallValues() { - methodCallExpressions.keySet().removeIf(MethodCall::isModifiableByOtherCode); - } - /** * Returns the new value of a field after a method call, or {@code null} if the field should be * removed from the store. From 7fabcdff5a08e0bd5b67d68013bbd94eef7f6604 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 17:46:56 -0700 Subject: [PATCH 092/308] Simplify test --- checker/tests/modifiability/SetRefinementTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 34f82bf2ae58..1bb9d22df1b2 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -7,6 +7,7 @@ // type (@UnknownModifiability), causing subsequent mutation calls to fail. public class SetRefinementTest { + // Flow-refinement behaves as if this assignment is at the beginning of every constructor body. private List items = new ArrayList<>(); /** Creates a new SetRefinementTest. */ @@ -18,25 +19,23 @@ public SetRefinementTest() {} * @param other list to copy from */ public SetRefinementTest(List other) { - this.items.addAll(other); // this should not be allowed but checker is not checking this + this.items.addAll(other); } public SetRefinementTest(List other, int dummy) { - // checker immediately sees that items' flow-type is @Modifiable, so this is ok this.items.set(0, other.get(0)); - // since set is not side-effect-free, we lose the @Modifiable refinement this.items.addAll(other); } public SetRefinementTest(List other, boolean dummy) { - this.items.set(0, other.get(0)); // this should not be allowed but checker is not checking this + this.items.set(0, other.get(0)); this.items.set(0, other.get(0)); this.items.addAll(other); } public SetRefinementTest(List other, float dummy) { - this.items.get(0); // get is side-effect-free, after this line, items is still @Modifiable - this.items.set(0, other.get(0)); // ok + this.items.get(0); + this.items.set(0, other.get(0)); this.items.addAll(other); } From 79d1594dd6faeed6af6382856793c3e0c9d81569 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 20:38:38 -0700 Subject: [PATCH 093/308] Add tests --- .../modifiability/SetRefinementTest.java | 89 +++++++++++++++++-- .../framework/flow/CFAbstractStore.java | 7 -- .../framework/type/AnnotatedTypeFactory.java | 10 --- 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 1bb9d22df1b2..00044706de2b 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -1,10 +1,11 @@ import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.modifiability.qual.Modifiable; + +// Tests that @DoesNotUnrefineReceiver on List.set() preserves the @Modifiable refinement of +// the receiver expression across different receiver kinds: field access, local variable, +// array access, and `this`. -// Reproduces a dataflow refinement issue: calling set() on a field initialized with -// new ArrayList<>() invalidates the @Modifiable refinement because set() lacks -// @SideEffectsOnly("this"). After the set() call, the field reverts to its declared -// type (@UnknownModifiability), causing subsequent mutation calls to fail. public class SetRefinementTest { // Flow-refinement behaves as if this assignment is at the beginning of every constructor body. @@ -13,12 +14,12 @@ public class SetRefinementTest { /** Creates a new SetRefinementTest. */ public SetRefinementTest() {} - /** - * Creates a new SetRefinementTest. - * - * @param other list to copy from - */ + // ========================================================== + // Field access as receiver + // ========================================================== + public SetRefinementTest(List other) { + // items is @Modifiable from the field initializer. this.items.addAll(other); } @@ -39,10 +40,80 @@ public SetRefinementTest(List other, float dummy) { this.items.addAll(other); } + // Negative field case: in a regular method, items has its declared type @UnknownModifiability. public void updateItemsLikeConstructor(List other, int dummy) { // :: error: [method.invocation] this.items.set(0, other.get(0)); // :: error: [method.invocation] this.items.addAll(other); } + + // ========================================================== + // Local variable as receiver + // + // In this checker (sideEffectsUnrefineAliases = false), local variable refinements are never + // cleared by side effects, so the behavior is correct regardless of @DoesNotUnrefineReceiver. + // These tests document the expected semantics. + // ========================================================== + + public void localVarReceiver(List other) { + List local = new ArrayList<>(); + local.set(0, other.get(0)); + local.addAll(other); + } + + public void localVarReceiverMultipleSets(List other) { + List local = new ArrayList<>(); + local.set(0, other.get(0)); + local.set(0, other.get(0)); + local.addAll(other); + } + + // ========================================================== + // Array access as receiver + // + // Array access refinements ARE always cleared by side effects (unlike local variables), so + // @DoesNotUnrefineReceiver makes an observable difference here. + // ========================================================== + + // Positive: set() preserves the @Modifiable refinement of the array element. + @SuppressWarnings("unchecked") + public void arrayAccessReceiver(List other) { + List[] arr = new ArrayList[1]; + arr[0] = new ArrayList<>(); + arr[0].set(0, other.get(0)); + arr[0].addAll(other); + } + + // Negative: add() lacks @DoesNotUnrefineReceiver, so the @Modifiable refinement is lost. + @SuppressWarnings("unchecked") + public void arrayAccessReceiverSideEffect(List other) { + List[] arr = new ArrayList[1]; + arr[0] = new ArrayList<>(); // arr[0] is @Modifiable + arr[0].add("x"); // side effect without @DoesNotUnrefineReceiver; refinement of arr[0] cleared + // :: error: [method.invocation] + arr[0].addAll(other); // ERROR: arr[0] is @UnknownModifiability (declared type) + } + + // ========================================================== + // `this` as receiver (when the class is itself a List) + // + // In this checker (sideEffectsUnrefineAliases = false), thisValue is never cleared by side + // effects, so the behavior is correct regardless of @DoesNotUnrefineReceiver. + // ========================================================== + + static class ThisListReceiver extends ArrayList { + + public void update(@Modifiable ThisListReceiver this, List other) { + this.set(0, other.get(0)); + this.addAll(other); + this.addAll(other); + } + + public void updateMultipleSets(@Modifiable ThisListReceiver this, List other) { + this.set(0, other.get(0)); + this.set(0, other.get(0)); + this.addAll(other); + } + } } 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 7edc98ce54bc..9e88a8a11c52 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -239,10 +239,7 @@ public void updateForMethodCall( boolean sideEffectsUnrefineAliases = gatypeFactory.sideEffectsUnrefineAliases; Node receiver = methodInvocationNode.getTarget().getReceiver(); - System.out.printf("receiver = %s [%s]%n", receiver, receiver.getClass()); boolean hasDoesNotUnrefineReceiver = atypeFactory.hasDoesNotUnrefineReceiver(method); - System.out.printf( - "hasDoesNotUnrefineReceiver(%s) = %s%n", method, hasDoesNotUnrefineReceiver); // TODO: Also remove if any element/argument to the annotation is not // isUnmodifiableByOtherCode. Example: @KeyFor("valueThatCanBeMutated"). @@ -273,7 +270,6 @@ public void updateForMethodCall( } // Update field values. - System.out.printf("sideEffectsUnrefineAliases = %s%n", sideEffectsUnrefineAliases); Predicate doNotUnrefineField = fa -> doNotUnrefine.test(fa); if (sideEffectsUnrefineAliases) { fieldValues @@ -395,7 +391,6 @@ protected V newMonotonicFieldValueAfterMethodCall( private void updateFieldValuesForMethodCall( GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefineField) { - System.out.printf("entering updateFieldValuesForMethodCall%n"); Map newFieldValues = new HashMap<>(MapsP.mapCapacity(fieldValues)); for (Map.Entry e : fieldValues.entrySet()) { FieldAccess fieldAccess = e.getKey(); @@ -403,8 +398,6 @@ private void updateFieldValuesForMethodCall( V newValue; boolean doNotUnrefineFieldResult = doNotUnrefineField.test(fieldAccess); - System.out.printf( - " doNotUnrefineFieldResult(%s) => %s%n", fieldAccess, doNotUnrefineFieldResult); if (doNotUnrefineFieldResult) { newValue = previousValue; } else { diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 5ca8b6b48451..ce958fa535a2 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -4050,13 +4050,6 @@ public boolean shouldWarnIfStubRedundantWithBytecode() { * @return all of the declaration annotations on this element, written in stub files, or inherited */ public AnnotationMirrorSet getDeclAnnotations(Element elt) { - boolean debug = false; - // debug = elt instanceof ExecutableElement; - if (debug) { - System.out.printf( - "Entering getDeclAnnotations(%s.%s)%n", - ((ExecutableElement) elt).getEnclosingElement().getSimpleName(), elt); - } AnnotationMirrorSet cachedValue = cacheDeclAnnos.get(elt); if (cachedValue != null) { // Found in cache, return result. @@ -4068,9 +4061,6 @@ public AnnotationMirrorSet getDeclAnnotations(Element elt) { // This includes annotations inherited from superclasses, but not superinterfaces or // overridden methods. List fromEle = elements.getAllAnnotationMirrors(elt); - if (debug) { - System.out.printf(" fromEle = %s%n", fromEle); - } for (AnnotationMirror annotation : fromEle) { try { results.add(annotation); From fbc9fa24a4e4306309c668cb9a19808f49080028 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 22:39:32 -0700 Subject: [PATCH 094/308] Javadoc --- .../dataflow/qual/DoesNotUnrefineReceiver.java | 2 ++ .../modifiability/grow/GrowAnnotatedTypeFactory.java | 10 ++++++++++ .../checker/modifiability/grow/GrowVisitor.java | 6 +++++- .../replace/ReplaceAnnotatedTypeFactory.java | 10 ++++++++++ .../checker/modifiability/replace/ReplaceVisitor.java | 6 +++++- .../shrink/ShrinkAnnotatedTypeFactory.java | 11 +++++++++++ .../checker/modifiability/shrink/ShrinkVisitor.java | 6 +++++- 7 files changed, 48 insertions(+), 3 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java index 22bef1387e67..0debc259be37 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java @@ -24,6 +24,8 @@ /** * The name of the checker that this annotation affects. For example, "modifiability" or * "nullness". Use the empty array to affect all checkers. + * + * @return the name of the checker that this annotation affects */ String[] value(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index c8b3b1b5a64b..f2d37c528aa4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -47,6 +47,11 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyGrow} qualifier. */ private AnnotationMirror POLY_GROW; + /** + * Creates a GrowAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ @SuppressWarnings("this-escape") public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -91,6 +96,11 @@ protected TypeAnnotator createTypeAnnotator() { *
*/ private class GrowTypeAnnotator extends TypeAnnotator { + /** + * Creates a new GrowTypeAnnotator. + * + * @param factory the associated type factory + */ public GrowTypeAnnotator(GrowAnnotatedTypeFactory factory) { super(factory); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java index d5b068d343c0..b0e4346eb142 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java @@ -6,7 +6,11 @@ /** Visitor for the {@link GrowChecker}. */ public class GrowVisitor extends ModifiabilityVisitor { - /** Create a visitor for the Grow Checker. */ + /** + * Creates a visitor for the Grow Checker. + * + * @param checker the Grow Checker + */ public GrowVisitor(BaseTypeChecker checker) { super(checker); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index e6ebaf999429..ee81dc0c3ff4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -56,6 +56,11 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyReplace} qualifier. */ private AnnotationMirror POLY_REPLACE; + /** + * Creates a ReplaceAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ @SuppressWarnings("this-escape") public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -107,6 +112,11 @@ protected TypeAnnotator createTypeAnnotator() { * */ private class ReplaceTypeAnnotator extends TypeAnnotator { + /** + * Creates a new ReplaceTypeAnnotator. + * + * @param factory the associated type factory + */ public ReplaceTypeAnnotator(ReplaceAnnotatedTypeFactory factory) { super(factory); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 55d17625b0c5..03dc7ea43e95 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -6,7 +6,11 @@ /** Visitor for the {@link ReplaceChecker}. */ public class ReplaceVisitor extends ModifiabilityVisitor { - /** Create a visitor for the Replace Checker. */ + /** + * Creates a visitor for the Replace Checker. + * + * @param checker the Replace Checker + */ public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index a7c6e653fc74..2e4275226ea9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -41,6 +41,11 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyShrink} qualifier. */ private AnnotationMirror POLY_SHRINK; + /** + * Creates a ShrinkAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ @SuppressWarnings("this-escape") public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -81,6 +86,12 @@ protected TypeAnnotator createTypeAnnotator() { * */ private class ShrinkTypeAnnotator extends TypeAnnotator { + + /** + * Creates a new ShrinkTypeAnnotator. + * + * @param factory the associated type factory + */ public ShrinkTypeAnnotator(ShrinkAnnotatedTypeFactory factory) { super(factory); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index febfe4a9a808..5d58b5382f85 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -6,7 +6,11 @@ /** Visitor for the {@link ShrinkChecker}. */ public class ShrinkVisitor extends ModifiabilityVisitor { - /** Create a visitor for the Shrink Checker. */ + /** + * Create a visitor for the Shrink Checker. + * + * @param checker the Shrink Checker + */ public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } From a9ce162acdd8d4ec13ea84fc218ba8971d29b3b2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 22:47:24 -0700 Subject: [PATCH 095/308] Rename variable --- .../framework/flow/CFAbstractStore.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 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 9e88a8a11c52..ca584392d01e 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -385,20 +385,19 @@ protected V newMonotonicFieldValueAfterMethodCall( * fields that have a monotonic annotation. * * @param atypeFactory AnnotatedTypeFactory of the associated checker - * @param doNotUnrefineField if true of a field access, don't unrefine it. This predicate - * indicates exceptions: fields that is not updated by this method. + * @param doNotUnrefine if true of a field access, don't unrefine it. This predicate indicates + * exceptions: fields that is not updated by this method. */ private void updateFieldValuesForMethodCall( - GenericAnnotatedTypeFactory atypeFactory, - Predicate doNotUnrefineField) { + GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefine) { Map newFieldValues = new HashMap<>(MapsP.mapCapacity(fieldValues)); for (Map.Entry e : fieldValues.entrySet()) { FieldAccess fieldAccess = e.getKey(); V previousValue = e.getValue(); V newValue; - boolean doNotUnrefineFieldResult = doNotUnrefineField.test(fieldAccess); - if (doNotUnrefineFieldResult) { + boolean doNotUnrefineResult = doNotUnrefine.test(fieldAccess); + if (doNotUnrefineResult) { newValue = previousValue; } else { newValue = newFieldValueAfterMethodCall(fieldAccess, atypeFactory, previousValue); From 8bd38800f58de76ccfa0fc0ca76f1ed66bfd4fec Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 22:55:16 -0700 Subject: [PATCH 096/308] Complete the feature --- .../nullness/qual/EnsuresNonNullIf.java | 2 +- .../qual/DoesNotUnrefineReceiver.java | 2 + docs/CHANGELOG.md | 2 +- .../framework/flow/CFAbstractStore.java | 69 ++++++++++++++----- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java index d74189c154ce..5d0fcf215b95 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java @@ -84,7 +84,7 @@ /** * Returns Java expression(s) that are non-null after the method returns the given result. * - * @return Java expression(s) that are non-null after the method returns the given result + * @return the Java expression(s) that are non-null after the method returns the given result * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions */ String[] expression(); diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java index 22bef1387e67..0debc259be37 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java @@ -24,6 +24,8 @@ /** * The name of the checker that this annotation affects. For example, "modifiability" or * "nullness". Use the empty array to affect all checkers. + * + * @return the name of the checker that this annotation affects */ String[] value(); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 39b15aeb9df4..7e7df90823b3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,7 +10,7 @@ ### Implementation details New method annotation `DoesNotUnrefineReceiver`. -Interface `AnnotationProvider` has a new method `isDoesNotUnrefineReceiver()`. +Interface `AnnotationProvider` has a new method `hasDoesNotUnrefineReceiver()`. ### Closed issues 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 fbd160e8a05f..46ffcec3f272 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -10,6 +10,7 @@ import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BinaryOperator; +import java.util.function.Predicate; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; @@ -237,32 +238,63 @@ public void updateForMethodCall( if (hasSideEffect) { boolean sideEffectsUnrefineAliases = gatypeFactory.sideEffectsUnrefineAliases; + Node receiver = methodInvocationNode.getTarget().getReceiver(); + boolean hasDoesNotUnrefineReceiver = atypeFactory.hasDoesNotUnrefineReceiver(method); - // update local variables // TODO: Also remove if any element/argument to the annotation is not // isUnmodifiableByOtherCode. Example: @KeyFor("valueThatCanBeMutated"). + + // If @DoesNotUnrefineReceiver is present, compute the receiver as a JavaExpression so + // that it can be exempted from unrefinement in all expression categories below. + @Nullable JavaExpression receiverJe = + hasDoesNotUnrefineReceiver ? JavaExpression.fromNode(receiver) : null; + // Returns true if the expression should NOT be unrefined (because the method is + // annotated @DoesNotUnrefineReceiver and the expression is the receiver). + Predicate doNotUnrefine = + receiverJe != null ? je -> je.equals(receiverJe) : je -> false; + + // Update local variables. if (sideEffectsUnrefineAliases) { - localVariableValues.entrySet().removeIf(e -> e.getKey().isModifiableByOtherCode()); + localVariableValues + .entrySet() + .removeIf( + e -> { + LocalVariable lv = e.getKey(); + return lv.isModifiableByOtherCode() && !doNotUnrefine.test(lv); + }); } - // update this value - if (sideEffectsUnrefineAliases && !atypeFactory.isDoesNotUnrefineReceiver(method)) { + // Update this value. + if (sideEffectsUnrefineAliases && !(receiverJe instanceof ThisReference)) { thisValue = null; } - // update field values + // Update field values. + Predicate doNotUnrefineField = fa -> doNotUnrefine.test(fa); if (sideEffectsUnrefineAliases) { - fieldValues.entrySet().removeIf(e -> e.getKey().isModifiableByOtherCode()); + fieldValues + .entrySet() + .removeIf( + (Map.Entry e) -> { + FieldAccess fa = e.getKey(); + return fa.isModifiableByOtherCode() && !doNotUnrefine.test(fa); + }); } else { - // Case 2 (unassignable fields) and case 3 (monotonic fields) - updateFieldValuesForMethodCall(gatypeFactory); + // Case 2 (unassignable fields) and case 3 (monotonic fields). + updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefineField); } // Update array values. - arrayValues.clear(); + arrayValues.entrySet().removeIf(e -> !doNotUnrefine.test(e.getKey())); // Update information about method calls. - updateMethodCallValues(); + methodCallExpressions + .entrySet() + .removeIf( + e -> { + MethodCall mc = e.getKey(); + return mc.isModifiableByOtherCode() && !doNotUnrefine.test(mc); + }); } // Store information about method calls if possible. @@ -270,11 +302,6 @@ public void updateForMethodCall( replaceValue(methodCall, val); } - /** Update information about method calls. */ - private void updateMethodCallValues() { - methodCallExpressions.keySet().removeIf(MethodCall::isModifiableByOtherCode); - } - /** * Returns the new value of a field after a method call, or {@code null} if the field should be * removed from the store. @@ -358,15 +385,23 @@ protected V newMonotonicFieldValueAfterMethodCall( * fields that have a monotonic annotation. * * @param atypeFactory AnnotatedTypeFactory of the associated checker + * @param doNotUnrefine if true of a field access, don't unrefine it. This predicate indicates + * exceptions: fields that is not updated by this method. */ private void updateFieldValuesForMethodCall( - GenericAnnotatedTypeFactory atypeFactory) { + GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefine) { Map newFieldValues = new HashMap<>(MapsP.mapCapacity(fieldValues)); for (Map.Entry e : fieldValues.entrySet()) { FieldAccess fieldAccess = e.getKey(); V previousValue = e.getValue(); - V newValue = newFieldValueAfterMethodCall(fieldAccess, atypeFactory, previousValue); + V newValue; + boolean doNotUnrefineResult = doNotUnrefine.test(fieldAccess); + if (doNotUnrefineResult) { + newValue = previousValue; + } else { + newValue = newFieldValueAfterMethodCall(fieldAccess, atypeFactory, previousValue); + } if (newValue != null) { // Keep information for all hierarchies where we had a monotonic annotation. newFieldValues.put(fieldAccess, newValue); From 0c665a5e783dc8a80479ac60370c1dd8252fbfe2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 22:57:35 -0700 Subject: [PATCH 097/308] Fix method name --- .../checkerframework/framework/type/AnnotatedTypeFactory.java | 2 +- .../java/org/checkerframework/javacutil/AnnotationProvider.java | 2 +- .../org/checkerframework/javacutil/BasicAnnotationProvider.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 1a193f7c00d4..4e9e1e251dea 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -5866,7 +5866,7 @@ public boolean isDeterministic(ExecutableElement methodElement) { } @Override - public boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement) { + public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { for (AnnotationMirror am : getDeclAnnotations(methodElement)) { if (areSameByClass(am, org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver.class)) { List typeSystems = diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java index 876d1a01ffc5..1ccb83f9ea31 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java @@ -64,5 +64,5 @@ public interface AnnotationProvider { * @param methodElement a method * @return true if the method does not unrefine its receiver */ - boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement); + boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement); } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java index 390627fc3af6..45e88acc8ecb 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java @@ -91,7 +91,7 @@ public boolean isDeterministic(ExecutableElement methodElement) { * present on the given method, with an empty "values" element. */ @Override - public boolean isDoesNotUnrefineReceiver(ExecutableElement methodElement) { + public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { for (AnnotationMirror am : methodElement.getAnnotationMirrors()) { if (AnnotationUtils.areSameByName( am, "org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver")) { From be6d64a9cf6da7ea0e609f7be7d704176ad68b76 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 23:33:50 -0700 Subject: [PATCH 098/308] CodeRabbit --- .../dataflow/qual/DoesNotUnrefineReceiver.java | 6 +++--- .../checkerframework/framework/flow/CFAbstractStore.java | 5 ++--- .../framework/type/GenericAnnotatedTypeFactory.java | 5 +++-- .../org/checkerframework/javacutil/AnnotationProvider.java | 3 +-- .../checkerframework/javacutil/BasicAnnotationProvider.java | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java index 0debc259be37..329ea505d64e 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java @@ -9,7 +9,7 @@ /** * At a call to a side-effecting method, the framework ordinarily discards all refined type * information, since the side-effecting method might invalidate that information. This annotation - * indicates that, at a call to the annotated method, the receiver's type should not be unrefinde. + * indicates that, at a call to the annotated method, the receiver's type should not be unrefined. * That is, a call to the method does not affect the type qualifier (in the given hierarchy). * * @checker_framework.manual #type-refinement-purity Side effects, determinism, purity, and @@ -22,8 +22,8 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) public @interface DoesNotUnrefineReceiver { /** - * The name of the checker that this annotation affects. For example, "modifiability" or - * "nullness". Use the empty array to affect all checkers. + * The name of the checker(s) that this annotation affects. For example, "modifiability" or + * "nullness". Use "allcheckers" to affect all checkers. * * @return the name of the checker that this annotation affects */ 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 ca584392d01e..0e7f86f2f4f0 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -270,7 +270,6 @@ public void updateForMethodCall( } // Update field values. - Predicate doNotUnrefineField = fa -> doNotUnrefine.test(fa); if (sideEffectsUnrefineAliases) { fieldValues .entrySet() @@ -281,7 +280,7 @@ public void updateForMethodCall( }); } else { // Case 2 (unassignable fields) and case 3 (monotonic fields). - updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefineField); + updateFieldValuesForMethodCall(gatypeFactory, doNotUnrefine::test); } // Update array values. @@ -386,7 +385,7 @@ protected V newMonotonicFieldValueAfterMethodCall( * * @param atypeFactory AnnotatedTypeFactory of the associated checker * @param doNotUnrefine if true of a field access, don't unrefine it. This predicate indicates - * exceptions: fields that is not updated by this method. + * exceptions: fields that are not updated by this method. */ private void updateFieldValuesForMethodCall( GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefine) { diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java index 79ad0f04c7c6..bdc8a192bf16 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1372,6 +1372,7 @@ protected void performFlowAnalysisForClass(ClassTree classTree) { methods.add(method); break; case VARIABLE: + // A top-level variable is a field. VariableTree vt = (VariableTree) m; ExpressionTree initializer = vt.getInitializer(); AnnotatedTypeMirror declaredType = getAnnotatedTypeLhs(vt); @@ -1387,8 +1388,8 @@ protected void performFlowAnalysisForClass(ClassTree classTree) { new CFGStatement(vt, ct), fieldValues, null, - true, - true, + /* isInitializationCode= */ true, + /* updateInitializationStore= */ true, isStatic, capturedStore); postAnalyze(cfg); diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java index 1ccb83f9ea31..5e2b84a222d6 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java @@ -58,8 +58,7 @@ public interface AnnotationProvider { boolean isDeterministic(ExecutableElement methodElement); /** - * Returns true if the given method may have side effects but does not does not unrefine its - * receiver. + * Returns true if the given method may have side effects but does not unrefine its receiver. * * @param methodElement a method * @return true if the method does not unrefine its receiver diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java index 45e88acc8ecb..4fdb3b3fefe4 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java @@ -88,7 +88,7 @@ public boolean isDeterministic(ExecutableElement methodElement) { * {@inheritDoc} * *

This implementation returns true if the {@code @DoesNotUnrefineReceiver} annotation is - * present on the given method, with an empty "values" element. + * present on the given method, with "allcheckers" in the {@code value} element. */ @Override public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { @@ -98,7 +98,7 @@ public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { @SuppressWarnings("deprecation") // calls from the framework are permitted List typeSystems = AnnotationUtils.getElementValueArray(am, "value", String.class, true); - return typeSystems.isEmpty(); + return typeSystems.contains("allcheckers"); } } return false; From 0d919c6759906680715577253b4685bb7fe85e3a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 10 Apr 2026 23:34:19 -0700 Subject: [PATCH 099/308] CodeRabbit --- .../org/checkerframework/framework/flow/CFAbstractStore.java | 2 +- .../org/checkerframework/javacutil/AnnotationProvider.java | 3 +-- .../checkerframework/javacutil/BasicAnnotationProvider.java | 4 ++-- 3 files changed, 4 insertions(+), 5 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 ca584392d01e..b5ace3e95b82 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -386,7 +386,7 @@ protected V newMonotonicFieldValueAfterMethodCall( * * @param atypeFactory AnnotatedTypeFactory of the associated checker * @param doNotUnrefine if true of a field access, don't unrefine it. This predicate indicates - * exceptions: fields that is not updated by this method. + * exceptions: fields that are not updated by this method. */ private void updateFieldValuesForMethodCall( GenericAnnotatedTypeFactory atypeFactory, Predicate doNotUnrefine) { diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java index 1ccb83f9ea31..5e2b84a222d6 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java @@ -58,8 +58,7 @@ public interface AnnotationProvider { boolean isDeterministic(ExecutableElement methodElement); /** - * Returns true if the given method may have side effects but does not does not unrefine its - * receiver. + * Returns true if the given method may have side effects but does not unrefine its receiver. * * @param methodElement a method * @return true if the method does not unrefine its receiver diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java index 45e88acc8ecb..4fdb3b3fefe4 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java @@ -88,7 +88,7 @@ public boolean isDeterministic(ExecutableElement methodElement) { * {@inheritDoc} * *

This implementation returns true if the {@code @DoesNotUnrefineReceiver} annotation is - * present on the given method, with an empty "values" element. + * present on the given method, with "allcheckers" in the {@code value} element. */ @Override public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { @@ -98,7 +98,7 @@ public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { @SuppressWarnings("deprecation") // calls from the framework are permitted List typeSystems = AnnotationUtils.getElementValueArray(am, "value", String.class, true); - return typeSystems.isEmpty(); + return typeSystems.contains("allcheckers"); } } return false; From a4994adeb6ce2a413ad04cfe2985717c6678c408 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 11 Apr 2026 08:52:16 -0700 Subject: [PATCH 100/308] Update framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../org/checkerframework/framework/flow/CFAbstractStore.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0e7f86f2f4f0..f8f86d303e3d 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -265,7 +265,9 @@ public void updateForMethodCall( } // Update this value. - if (sideEffectsUnrefineAliases && !(receiverJe instanceof ThisReference)) { + if (sideEffectsUnrefineAliases + && !(receiverJe instanceof ThisReference) + && !(receiverJe instanceof SuperReference)) { thisValue = null; } From b9cf5e99c9c777554231410cb2c8f5da46a6f65f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 11 Apr 2026 09:27:23 -0700 Subject: [PATCH 101/308] Brevity --- .../checkerframework/checker/modifiability/messages.properties | 2 +- .../framework/test/diagnostics/TestDiagnosticUtils.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index 7eda6bdc0bf3..95de156cf40a 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1 +1 @@ -usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. +usage.throws.uoe=Method %s always throws an UnsupportedOperationException diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java index 3ae647bc191e..bcb5a4d25890 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java @@ -297,7 +297,7 @@ private static boolean retainAllLines(String message) { /** * Given a category string that may be prepended with "fixable-", return the category enum that - * corresponds with the category and whether or not it is an isFixable error + * corresponds with the category and whether or not it is an isFixable error. * * @param category a category string that may be prepended with "fixable-" * @return a pair of the category and whether it was prepended with "fixable-" From 7c22e0fec1aa18b5eaae2bb33a4c365213bfe41f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 12 Apr 2026 21:53:59 -0700 Subject: [PATCH 102/308] Change location for cloning the JDK --- checker/bin-devel/clone-related.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/bin-devel/clone-related.sh b/checker/bin-devel/clone-related.sh index 134bbaf37431..5e6a743ca1bb 100755 --- a/checker/bin-devel/clone-related.sh +++ b/checker/bin-devel/clone-related.sh @@ -49,7 +49,7 @@ else fi # Clone the annotated JDK into ../jdk . -"$GIT_SCRIPTS/git-clone-related" ${DEBUG_FLAG} typetools jdk +"$GIT_SCRIPTS/git-clone-related" ${DEBUG_FLAG} typetools ../jdk ### Commented temporarily because JSpecify build is failing under JDK 17. ### (I guess they don't use continuous integration.) From a0e2453974b9cf5f888bc09238c98ae3efe267fe Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 12 Apr 2026 22:09:24 -0700 Subject: [PATCH 103/308] Add todo --- checker/bin-devel/clone-related.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/bin-devel/clone-related.sh b/checker/bin-devel/clone-related.sh index 5e6a743ca1bb..8b3cb08f1eb0 100755 --- a/checker/bin-devel/clone-related.sh +++ b/checker/bin-devel/clone-related.sh @@ -49,7 +49,8 @@ else fi # Clone the annotated JDK into ../jdk . -"$GIT_SCRIPTS/git-clone-related" ${DEBUG_FLAG} typetools ../jdk +# TODO: Document why the command line uses "jdk" rather than "../jdk". +"$GIT_SCRIPTS/git-clone-related" ${DEBUG_FLAG} typetools jdk ### Commented temporarily because JSpecify build is failing under JDK 17. ### (I guess they don't use continuous integration.) From 04473f59a873bd03002492f3cfbdb085b9cb48f8 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 17 Apr 2026 17:37:46 -0700 Subject: [PATCH 104/308] change the 2 type lattice to four type lattice; update modifiability-checker manual and tests accordingly. TODO: need to add @Ungrowable to the return type of entryset, values, and keyset. --- buildSrc/src/main/groovy/cf-spotless.gradle | 6 + .../modifiability/qual/BottomGrow.java | 21 +++ .../modifiability/qual/BottomReplace.java | 21 +++ .../modifiability/qual/BottomShrink.java | 21 +++ .../modifiability/qual/Ungrowable.java | 22 +++ .../modifiability/qual/UnknownGrow.java | 4 +- .../qual/UnknownModifiability.java | 3 +- .../modifiability/qual/UnknownReplace.java | 6 +- .../modifiability/qual/UnknownShrink.java | 6 +- .../modifiability/qual/Unmodifiable.java | 6 +- .../modifiability/qual/Unreplaceable.java | 22 +++ .../modifiability/qual/Unshrinkable.java | 22 +++ .../grow/GrowAnnotatedTypeFactory.java | 14 +- .../modifiability/grow/GrowChecker.java | 6 +- .../replace/ReplaceAnnotatedTypeFactory.java | 17 +- .../modifiability/replace/ReplaceChecker.java | 6 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 19 +- .../modifiability/shrink/ShrinkChecker.java | 6 +- .../tests/modifiability/HierarchyTest.java | 168 +++++++++++------- .../modifiability/PolyModifiableTest.java | 5 +- docs/manual/modifiability-checker.tex | 82 ++++++--- 21 files changed, 360 insertions(+), 123 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index b7f29b8576d5..6b750e1cf9fb 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -92,6 +92,9 @@ spotless { googleJavaFormat(libs.google.java.format.get().version) } formatAnnotations() + .addTypeAnnotation("BottomGrow") + .addTypeAnnotation("BottomReplace") + .addTypeAnnotation("BottomShrink") .addTypeAnnotation("Growable") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrow") @@ -104,6 +107,9 @@ spotless { .addTypeAnnotation("UnknownModifiability") .addTypeAnnotation("UnknownReplace") .addTypeAnnotation("UnknownShrink") + .addTypeAnnotation("Ungrowable") + .addTypeAnnotation("Unreplaceable") + .addTypeAnnotation("Unshrinkable") .addTypeAnnotation("Unmodifiable") } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java new file mode 100644 index 000000000000..a01b989a8664 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.modifiability.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 bottom qualifier in the Grow hierarchy. + * + *

Programmers should rarely write this qualifier. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Growable.class, Ungrowable.class}) +public @interface BottomGrow {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java new file mode 100644 index 000000000000..a2609e3558de --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.modifiability.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 bottom qualifier in the Replace hierarchy. + * + *

Programmers should rarely write this qualifier. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Replaceable.class, Unreplaceable.class}) +public @interface BottomReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java new file mode 100644 index 000000000000..3cf5d6902f6e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.modifiability.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 bottom qualifier in the Shrink hierarchy. + * + *

Programmers should rarely write this qualifier. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Shrinkable.class, Unshrinkable.class}) +public @interface BottomShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java new file mode 100644 index 000000000000..24348a29d853 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling grow operations such as {@code add}, {@code addAll}, etc. on this collection will throw + * {@link UnsupportedOperationException}. + * + *

No guarantees are made about shrink or replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownGrow.class) +public @interface Ungrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java index 30f3c460deec..c71e04417b1c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java @@ -9,8 +9,8 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Grow hierarchy. Represents an unknown or absent grow capability. The - * checker cannot determine whether the collection supports grow operations such as {@code add}. + * The top qualifier in the Grow hierarchy. Represents unknown grow capability. The checker cannot + * determine whether the collection supports grow operations such as {@code add}. * *

This is the default qualifier for unannotated types in the Grow hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java index f653f09d2d03..502805c4a134 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java @@ -12,8 +12,7 @@ * collection is growable, shrinkable, or replaceable. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @UnknownGrow @UnknownShrink @UnknownReplace} on each annotated type. It is equivalent to - * {@link Unmodifiable}. + * {@code @UnknownGrow @UnknownShrink @UnknownReplace} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java index a793d64f521f..164002371c5b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java @@ -9,9 +9,9 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Replace hierarchy. Represents an unknown or absent replace capability. - * The checker cannot determine whether the collection supports replace operations such as {@code - * set} or {@code replaceAll}. + * The top qualifier in the Replace hierarchy. Represents unknown replace capability. The checker + * cannot determine whether the collection supports replace operations such as {@code set} or {@code + * replaceAll}. * *

This is the default qualifier for unannotated types in the Replace hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java index 7f87345b521c..68f5918995e3 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java @@ -9,9 +9,9 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Shrink hierarchy. Represents an unknown or absent shrink capability. The - * checker cannot determine whether the collection supports shrink operations such as {@code remove} - * or {@code clear}. + * The top qualifier in the Shrink hierarchy. Represents unknown shrink capability. The checker + * cannot determine whether the collection supports shrink operations such as {@code remove} or + * {@code clear}. * *

This is the default qualifier for unannotated types in the Shrink hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index f5252ff71572..f8d2c6fdc043 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -7,12 +7,12 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @UnknownGrow @UnknownShrink @UnknownReplace}. Calling any - * mutating operation on this collection (growing, shrinking, or replacing) will throw {@link + * Convenience alias meaning {@code @Ungrowable @Unshrinkable @Unreplaceable}. Calling any mutating + * operation on this collection (growing, shrinking, or replacing) will throw {@link * UnsupportedOperationException}. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @UnknownGrow @UnknownShrink @UnknownReplace} on each annotated type. + * {@code @Ungrowable @Unshrinkable @Unreplaceable} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java new file mode 100644 index 000000000000..1f58bc15625f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling replace operations such as {@code set}, {@code replaceAll}, etc. on this collection will + * throw {@link UnsupportedOperationException}. + * + *

No guarantees are made about grow or shrink operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownReplace.class) +public @interface Unreplaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java new file mode 100644 index 000000000000..1b97e46abbdf --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling shrink operations such as {@code remove}, {@code clear}, etc. on this collection will + * throw {@link UnsupportedOperationException}. + * + *

No guarantees are made about grow or replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownShrink.class) +public @interface Unshrinkable {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index c8b3b1b5a64b..5d8f449ad758 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -7,10 +7,12 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.BottomGrow; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrow; import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.UnknownGrow; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -41,9 +43,12 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ private AnnotationMirror UNKNOWN_GROW; - /** The {@code @}{@link Growable} qualifier (bottom of Grow hierarchy). */ + /** The {@code @}{@link Growable} qualifier. */ private AnnotationMirror GROWABLE; + /** The {@code @}{@link Ungrowable} qualifier. */ + private AnnotationMirror UNGROWABLE; + /** The {@code @}{@link PolyGrow} qualifier. */ private AnnotationMirror POLY_GROW; @@ -61,10 +66,11 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { // Initialize annotation mirrors after the hierarchy is established. this.UNKNOWN_GROW = AnnotationBuilder.fromClass(getElementUtils(), UnknownGrow.class); this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); + this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); addAliasedTypeAnnotation(Modifiable.class, GROWABLE); - addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_GROW); + addAliasedTypeAnnotation(Unmodifiable.class, UNGROWABLE); addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_GROW); addAliasedTypeAnnotation(PolyModifiable.class, POLY_GROW); postInit(); @@ -72,7 +78,9 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { @Override protected Set> createSupportedTypeQualifiers() { - return new LinkedHashSet<>(Arrays.asList(UnknownGrow.class, Growable.class, PolyGrow.class)); + return new LinkedHashSet<>( + Arrays.asList( + UnknownGrow.class, Growable.class, Ungrowable.class, BottomGrow.class, PolyGrow.class)); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 5ca4b91355d8..e15a1dec243f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a grow method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Growable} collections can be - * safely mutated and {@code @UnknownGrow} collections cannot call add without risking an {@link - * UnsupportedOperationException}. + * safely mutated, {@code @Ungrowable} collections cannot be grown, and {@code @UnknownGrow} + * collections have unknown grow behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @@ -27,7 +27,7 @@ Collections.class }) @StubFiles({"ical4j.astub", "javaparser.astub"}) -@SuppressWarningsPrefix({"Growable", "UnknownGrow"}) +@SuppressWarningsPrefix({"Growable", "Ungrowable", "UnknownGrow", "BottomGrow"}) public class GrowChecker extends BaseTypeChecker { /** Creates a Grow checker. */ public GrowChecker() {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index e6ebaf999429..2a1a7b42dfe1 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -7,6 +7,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.BottomReplace; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplace; @@ -14,6 +15,7 @@ import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownReplace; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; @@ -50,9 +52,12 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ private AnnotationMirror UNKNOWN_REPLACE; - /** The {@code @}{@link Replaceable} qualifier (bottom of Replace hierarchy). */ + /** The {@code @}{@link Replaceable} qualifier. */ private AnnotationMirror REPLACEABLE; + /** The {@code @}{@link Unreplaceable} qualifier. */ + private AnnotationMirror UNREPLACEABLE; + /** The {@code @}{@link PolyReplace} qualifier. */ private AnnotationMirror POLY_REPLACE; @@ -75,10 +80,11 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { // Initialize annotation mirrors after the hierarchy is established. this.UNKNOWN_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), UnknownReplace.class); this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); + this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); addAliasedTypeAnnotation(Modifiable.class, REPLACEABLE); - addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_REPLACE); + addAliasedTypeAnnotation(Unmodifiable.class, UNREPLACEABLE); addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_REPLACE); addAliasedTypeAnnotation(PolyModifiable.class, POLY_REPLACE); postInit(); @@ -87,7 +93,12 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( - Arrays.asList(UnknownReplace.class, Replaceable.class, PolyReplace.class)); + Arrays.asList( + UnknownReplace.class, + Replaceable.class, + Unreplaceable.class, + BottomReplace.class, + PolyReplace.class)); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index f705ca9fbe54..52aa1ecf7c70 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a replace method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Replaceable} collections can - * be safely mutated and {@code @UnknownReplace} collections cannot call remove without risking an - * {@link UnsupportedOperationException}. + * be safely mutated, {@code @Unreplaceable} collections cannot be replaced, and + * {@code @UnknownReplace} collections have unknown replace behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @@ -27,7 +27,7 @@ Collections.class }) @StubFiles({"ical4j.astub", "javaparser.astub"}) -@SuppressWarningsPrefix({"Replaceable", "UnknownReplace"}) +@SuppressWarningsPrefix({"Replaceable", "Unreplaceable", "UnknownReplace", "BottomReplace"}) public class ReplaceChecker extends BaseTypeChecker { /** Creates a Replace checker. */ public ReplaceChecker() {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index a7c6e653fc74..d80d88cb9630 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -7,6 +7,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.BottomShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; @@ -14,6 +15,7 @@ import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; @@ -35,9 +37,12 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ private AnnotationMirror UNKNOWN_SHRINK; - /** The {@code @}{@link Shrinkable} qualifier (bottom of Shrink hierarchy). */ + /** The {@code @}{@link Shrinkable} qualifier. */ private AnnotationMirror SHRINKABLE; + /** The {@code @}{@link Unshrinkable} qualifier. */ + private AnnotationMirror UNSHRINKABLE; + /** The {@code @}{@link PolyShrink} qualifier. */ private AnnotationMirror POLY_SHRINK; @@ -52,10 +57,11 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { // Initialize annotation mirrors after the hierarchy is established. this.UNKNOWN_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), UnknownShrink.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); + this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); - addAliasedTypeAnnotation(Unmodifiable.class, UNKNOWN_SHRINK); + addAliasedTypeAnnotation(Unmodifiable.class, UNSHRINKABLE); addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_SHRINK); addAliasedTypeAnnotation(PolyModifiable.class, POLY_SHRINK); postInit(); @@ -64,7 +70,12 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( - Arrays.asList(UnknownShrink.class, Shrinkable.class, PolyShrink.class)); + Arrays.asList( + UnknownShrink.class, + Shrinkable.class, + Unshrinkable.class, + BottomShrink.class, + PolyShrink.class)); } @Override @@ -97,7 +108,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { TypeMirror underlyingType = type.getUnderlyingType(); if (TypesUtils.isErasedSubtype(underlyingType, mapEntryErasure, types)) { - // Map.Entry: Drop G and S bits + // Map.Entry: no shrink. type.replaceAnnotation(UNKNOWN_SHRINK); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index f857ef7ea32d..9036e792fbf8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a shrink method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Shrinkable} collections can - * be safely mutated and {@code @UnknownShrink} collections cannot call remove without risking an - * {@link UnsupportedOperationException}. + * be safely mutated, {@code @Unshrinkable} collections cannot be shrunk, and {@code @UnknownShrink} + * collections have unknown shrink behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @@ -27,7 +27,7 @@ Collections.class }) @StubFiles({"ical4j.astub", "javaparser.astub"}) -@SuppressWarningsPrefix({"Shrinkable", "UnknownShrink"}) +@SuppressWarningsPrefix({"Shrinkable", "Unshrinkable", "UnknownShrink", "BottomShrink"}) public class ShrinkChecker extends BaseTypeChecker { /** Creates a Shrink checker. */ public ShrinkChecker() {} diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index 428bd4f19f0d..d412890cde4c 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -1,109 +1,152 @@ +import org.checkerframework.checker.modifiability.qual.BottomGrow; +import org.checkerframework.checker.modifiability.qual.BottomReplace; +import org.checkerframework.checker.modifiability.qual.BottomShrink; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.UnknownGrow; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownReplace; import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.Unreplaceable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; /** - * Tests the three independent 2-element lattices of the Modifiability Checker. + * Tests the three independent 4-element lattices of the Modifiability Checker. * - *

Grow hierarchy: UnknownGrow (top/default) ← Growable (bottom) Shrink hierarchy: UnknownShrink - * (top/default) ← Shrinkable (bottom) Replace hierarchy: UnknownReplace (top/default) ← Replaceable - * (bottom) + *

Each hierarchy is: Unknown* (top/default), two incomparable siblings (*able and Un*able), and + * Bottom* (bottom). * *

An annotated type carries one qualifier per hierarchy. Assignment is valid if and only if the * RHS is a subtype in every hierarchy simultaneously. */ class HierarchyTest { - // ============================================================ - // Grow hierarchy - // ============================================================ - void testGrow(@Growable Object g, @UnknownGrow Object u) { - // Growable <: UnknownGrow (bottom to top is always OK) + void testGrow( + @Growable Object g, @Ungrowable Object ug, @UnknownGrow Object u, @BottomGrow Object b) { @UnknownGrow Object u1 = g; + @UnknownGrow Object u2 = ug; + @UnknownGrow Object u3 = b; + + @Growable Object g1 = b; + @Ungrowable Object ug1 = b; + + // :: error: [assignment] + @Growable Object g2 = ug; + // :: error: [assignment] + @Ungrowable Object ug2 = g; + // :: error: [assignment] + @Growable Object g3 = u; + // :: error: [assignment] + @Ungrowable Object ug3 = u; + // :: error: [assignment] + @BottomGrow Object b1 = g; // :: error: [assignment] - @Growable Object g1 = u; // UnknownGrow !<: Growable + @BottomGrow Object b2 = ug; + // :: error: [assignment] + @BottomGrow Object b3 = u; } - // ============================================================ - // Shrink hierarchy - // ============================================================ - void testShrink(@Shrinkable Object s, @UnknownShrink Object u) { + void testShrink( + @Shrinkable Object s, + @Unshrinkable Object us, + @UnknownShrink Object u, + @BottomShrink Object b) { @UnknownShrink Object u1 = s; + @UnknownShrink Object u2 = us; + @UnknownShrink Object u3 = b; + + @Shrinkable Object s1 = b; + @Unshrinkable Object us1 = b; + + // :: error: [assignment] + @Shrinkable Object s2 = us; // :: error: [assignment] - @Shrinkable Object s1 = u; + @Unshrinkable Object us2 = s; + // :: error: [assignment] + @Shrinkable Object s3 = u; + // :: error: [assignment] + @Unshrinkable Object us3 = u; } - // ============================================================ - // Replace hierarchy - // ============================================================ - void testReplace(@Replaceable Object r, @UnknownReplace Object u) { + void testReplace( + @Replaceable Object r, + @Unreplaceable Object ur, + @UnknownReplace Object u, + @BottomReplace Object b) { @UnknownReplace Object u1 = r; + @UnknownReplace Object u2 = ur; + @UnknownReplace Object u3 = b; + + @Replaceable Object r1 = b; + @Unreplaceable Object ur1 = b; + + // :: error: [assignment] + @Replaceable Object r2 = ur; + // :: error: [assignment] + @Unreplaceable Object ur2 = r; + // :: error: [assignment] + @Replaceable Object r3 = u; // :: error: [assignment] - @Replaceable Object r1 = u; + @Unreplaceable Object ur3 = u; } - // ============================================================ - // Multi-annotation combinations (one qualifier per hierarchy) - // ============================================================ void testCombinations( - @Growable @Shrinkable @Replaceable Object gsr, // full capabilities - @Growable @Shrinkable Object gs, // Grow + Shrink only (Replace = UnknownReplace default) - @Growable @Replaceable Object gr, // Grow + Replace only (Shrink = UnknownShrink default) - @Shrinkable @Replaceable Object sr, // Shrink + Replace only (Grow = UnknownGrow default) - @Growable Object g, // Grow only - @Shrinkable Object s, // Shrink only - @Replaceable Object r, // Replace only - @UnknownGrow @UnknownShrink @UnknownReplace Object none) { // no capabilities + @Growable @Shrinkable @Replaceable Object gsr, + @Growable @Shrinkable Object gs, + @Growable @Replaceable Object gr, + @Shrinkable @Replaceable Object sr, + @Growable Object g, + @Shrinkable Object s, + @Replaceable Object r, + @Ungrowable @Unshrinkable @Unreplaceable Object none, + @UnknownGrow @UnknownShrink @UnknownReplace Object unknown) { - // ── Assignments to @Growable ────────────────────────────── - @Growable Object gv1 = gsr; // G+S+R <: G - @Growable Object gv2 = gs; // G+S <: G - @Growable Object gv3 = gr; // G+R <: G + @Growable Object gv1 = gsr; + @Growable Object gv2 = gs; + @Growable Object gv3 = gr; // :: error: [assignment] - @Growable Object gv4 = sr; // S+R: no G bit - @Growable Object gv5 = g; + @Growable Object gv4 = sr; // :: error: [assignment] - @Growable Object gv6 = s; + @Growable Object gv5 = s; // :: error: [assignment] - @Growable Object gv7 = r; + @Growable Object gv6 = r; // :: error: [assignment] - @Growable Object gv8 = none; + @Growable Object gv7 = none; + // :: error: [assignment] + @Growable Object gv8 = unknown; - // ── Assignments to @Shrinkable ──────────────────────────── @Shrinkable Object sv1 = gsr; @Shrinkable Object sv2 = gs; // :: error: [assignment] - @Shrinkable Object sv3 = gr; // G+R: no S bit + @Shrinkable Object sv3 = gr; @Shrinkable Object sv4 = sr; // :: error: [assignment] @Shrinkable Object sv5 = g; - @Shrinkable Object sv6 = s; // :: error: [assignment] - @Shrinkable Object sv7 = r; + @Shrinkable Object sv6 = r; + // :: error: [assignment] + @Shrinkable Object sv7 = none; // :: error: [assignment] - @Shrinkable Object sv8 = none; + @Shrinkable Object sv8 = unknown; - // ── Assignments to @Replaceable ─────────────────────────── @Replaceable Object rv1 = gsr; // :: error: [assignment] - @Replaceable Object rv2 = gs; // G+S: no R bit + @Replaceable Object rv2 = gs; @Replaceable Object rv3 = gr; @Replaceable Object rv4 = sr; // :: error: [assignment] @Replaceable Object rv5 = g; // :: error: [assignment] @Replaceable Object rv6 = s; - @Replaceable Object rv7 = r; // :: error: [assignment] - @Replaceable Object rv8 = none; + @Replaceable Object rv7 = none; + // :: error: [assignment] + @Replaceable Object rv8 = unknown; - // ── Assignments to full @UnknownGrow @UnknownShrink @UnknownReplace (top in all) ── @UnknownGrow @UnknownShrink @UnknownReplace Object tv1 = gsr; @UnknownGrow @UnknownShrink @UnknownReplace Object tv2 = gs; @UnknownGrow @UnknownShrink @UnknownReplace Object tv3 = gr; @@ -112,34 +155,33 @@ void testCombinations( @UnknownGrow @UnknownShrink @UnknownReplace Object tv6 = s; @UnknownGrow @UnknownShrink @UnknownReplace Object tv7 = r; @UnknownGrow @UnknownShrink @UnknownReplace Object tv8 = none; + @UnknownGrow @UnknownShrink @UnknownReplace Object tv9 = unknown; } - // ============================================================ - // Alias annotations - // ============================================================ void testAliases( @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object unknown) { - // @Modifiable expands to @Growable @Shrinkable @Replaceable (full capabilities) @Growable Object g1 = mod; @Shrinkable Object s1 = mod; @Replaceable Object r1 = mod; - // @Unmodifiable / @UnknownModifiability expand to @UnknownGrow @UnknownShrink @UnknownReplace - @UnknownGrow Object ug1 = unmod; - @UnknownShrink Object us1 = unmod; - @UnknownReplace Object ur1 = unmod; + @Ungrowable Object ug1 = unmod; + @Unshrinkable Object us1 = unmod; + @Unreplaceable Object ur1 = unmod; + @UnknownGrow Object ug2 = unknown; + @UnknownShrink Object us2 = unknown; + @UnknownReplace Object ur2 = unknown; - // @Modifiable <: @Unmodifiable in every hierarchy - @Unmodifiable Object unmod1 = mod; @UnknownModifiability Object unknown1 = mod; + @UnknownModifiability Object unknown2 = unmod; - // @Unmodifiable !<: @Growable (top !<: bottom in Grow hierarchy) + // :: error: [assignment] + @Unmodifiable Object unmod1 = mod; + // :: error: [assignment] + @Unmodifiable Object unmod2 = unknown; // :: error: [assignment] @Growable Object g2 = unmod; - - // @Unmodifiable !<: @Modifiable // :: error: [assignment] @Modifiable Object mod1 = unmod; } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 9f6e5db69e08..473d2e4ffcab 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -105,10 +105,11 @@ void testPoly( @Growable List u3 = identity(unknown); // Error // ============================================================ - // Identity on @Unmodifiable (alias for all tops) + // Identity on @Unmodifiable (Ungrowable+Unshrinkable+Unreplaceable) // ============================================================ @Unmodifiable List unmod1 = identity(unmod); // OK + @UnknownModifiability List unmod2 = identity(unmod); // OK // :: error: [assignment] - @Modifiable List unmod2 = identity(unmod); // Error + @Modifiable List unmod3 = identity(unmod); // Error } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 0f9818e09450..20a3a71a4996 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -61,7 +61,7 @@ \sectionAndLabel{Modifiability annotations}{modifiability-annotations} -The Modifiability Checker uses \textbf{three independent 2-element +The Modifiability Checker uses \textbf{three independent 4-element hierarchies}, one per capability. Every annotated type carries exactly one qualifier from each hierarchy. @@ -70,11 +70,13 @@ \centering \begin{minipage}{0.85\linewidth} \begin{verbatim} - Grow hierarchy Shrink hierarchy Replace hierarchy + Grow hierarchy Shrink hierarchy Replace hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace - | | | - @Growable @Shrinkable @Replaceable + @UnknownGrow @UnknownShrink @UnknownReplace + / \ / \ / \ + @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable + \ / \ / \ / + @BottomGrow @BottomShrink @BottomReplace \end{verbatim} \end{minipage} \caption{The three independent Modifiability qualifier hierarchies. @@ -88,8 +90,9 @@ example, \code{@Growable @UnknownShrink @UnknownReplace List} is a subtype of \code{@UnknownGrow @UnknownShrink @UnknownReplace List} (because \<@Growable> $\le$ \<@UnknownGrow>), but it is \emph{not} a -subtype of \code{@Growable @Shrinkable @UnknownReplace List} (because -\<@UnknownShrink> $\not\le$ \<@Shrinkable>). +subtype of \code{@Ungrowable @UnknownShrink @UnknownReplace List} +(because \<@Growable> and \<@Ungrowable> are incomparable siblings in the +Grow hierarchy). \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} @@ -102,11 +105,19 @@ Grow hierarchy. \item[\refqualclass{checker/modifiability/qual}{Growable}] -The bottom qualifier in the Grow hierarchy. Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this collection will not result in throwing \code{UnsupportedOperationException}. No guarantees are made about shrink or replace operations. +\item[\refqualclass{checker/modifiability/qual}{Ungrowable}] +Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this +collection will throw \code{UnsupportedOperationException}. +No guarantees are made about shrink or replace operations. + +\item[\refqualclass{checker/modifiability/qual}{BottomGrow}] +The bottom qualifier in the Grow hierarchy. It is a subtype of both +\<@Growable> and \<@Ungrowable>. Programmers should rarely write it. + \item[\refqualclass{checker/modifiability/qual}{UnknownShrink}] The top qualifier in the Shrink hierarchy. The checker cannot determine whether this collection supports shrink @@ -114,12 +125,20 @@ Shrink hierarchy. \item[\refqualclass{checker/modifiability/qual}{Shrinkable}] -The bottom qualifier in the Shrink hierarchy. Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on this collection will not result in throwing \code{UnsupportedOperationException}. No guarantees are made about grow or replace operations. +\item[\refqualclass{checker/modifiability/qual}{Unshrinkable}] +Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on +this collection will throw \code{UnsupportedOperationException}. +No guarantees are made about grow or replace operations. + +\item[\refqualclass{checker/modifiability/qual}{BottomShrink}] +The bottom qualifier in the Shrink hierarchy. It is a subtype of both +\<@Shrinkable> and \<@Unshrinkable>. Programmers should rarely write it. + \item[\refqualclass{checker/modifiability/qual}{UnknownReplace}] The top qualifier in the Replace hierarchy. The checker cannot determine whether this collection supports replace @@ -127,12 +146,20 @@ Replace hierarchy. \item[\refqualclass{checker/modifiability/qual}{Replaceable}] -The bottom qualifier in the Replace hierarchy. Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ on this collection will not result in throwing \code{UnsupportedOperationException}. No guarantees are made about grow or shrink operations. +\item[\refqualclass{checker/modifiability/qual}{Unreplaceable}] +Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ +on this collection will throw \code{UnsupportedOperationException}. +No guarantees are made about grow or shrink operations. + +\item[\refqualclass{checker/modifiability/qual}{BottomReplace}] +The bottom qualifier in the Replace hierarchy. It is a subtype of both +\<@Replaceable> and \<@Unreplaceable>. Programmers should rarely write it. + \end{description} \subsectionAndLabel{Combining capabilities}{modifiability-combining} @@ -166,14 +193,13 @@ \code{UnsupportedOperationException}. \item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] -Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. +Alias for \code{@Ungrowable @Unshrinkable @Unreplaceable}. The collection is definitely unmodifiable: calling any mutating operation always throws \code{UnsupportedOperationException}. \item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. The checker cannot determine the modifiability of the collection. -Equivalent to \<@Unmodifiable>. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrow @PolyShrink @PolyReplace} @@ -187,7 +213,7 @@ \begin{Verbatim} @Modifiable List mod = new ArrayList<>(); // all three capabilities @Unmodifiable List unmod = List.of("a"); // no capabilities - @UnknownModifiability List unknown = ...; // same as @Unmodifiable + @UnknownModifiability List unknown = ...; // capability is unknown \end{Verbatim} @@ -260,8 +286,8 @@ Polymorphic qualifier for the Shrink hierarchy. The return type's Shrink qualifier matches the Shrink qualifier of whichever formal parameter is annotated with \<@PolyShrink>. -Useful for methods such as \code{Map.keySet()} that strip Grow and -Replace but preserve Shrink. +Useful for methods such as \code{Map.keySet()} that preserve Shrink while +returning an \<@Ungrowable> view. \item[\refqualclass{checker/modifiability/qual}{PolyReplace}] Polymorphic qualifier for the Replace hierarchy. @@ -281,17 +307,20 @@ polymorphic and concrete qualifiers across hierarchies. For example: \begin{Verbatim} - // Shrink is preserved from the map; Grow and Replace are not. - @PolyShrink Set keySet(@PolyShrink Map map) + // Shrink is preserved from the map; result is explicitly @Ungrowable. + @PolyShrink @Ungrowable Set keySet(@PolyShrink Map map) \end{Verbatim} -If the map is \code{@Growable @Shrinkable @UnknownReplace}, then -\code{keySet} returns \code{@UnknownGrow @Shrinkable @UnknownReplace Set} -(after the Modifiability Checker also strips Replace from the \code{Set} -return type, which was already \<@UnknownReplace>). +If the map is \code{@Growable @Shrinkable @Replaceable}, then +\code{keySet} returns +\code{@Ungrowable @Shrinkable @UnknownReplace Set}. + +If the map is \code{@UnknownGrow @UnknownShrink @UnknownReplace}, then +\code{keySet} returns \code{@Ungrowable @UnknownShrink @UnknownReplace Set}. + +If the map is \code{@Ungrowable @Unshrinkable @Unreplaceable}, then +\code{keySet} returns \code{@Ungrowable @Unshrinkable @UnknownReplace Set}. -If the map is \code{@Growable @UnknownShrink @UnknownReplace}, then -\code{keySet} returns \code{@UnknownGrow @UnknownShrink @UnknownReplace Set}. \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} @@ -328,9 +357,9 @@ mod.remove("a"); // OK: @Shrinkable guarantees remove() works mod.set(0, "b"); // OK: @Replaceable guarantees set() works - // @Unmodifiable is an alias for @UnknownGrow @UnknownShrink @UnknownReplace + // @Unmodifiable is an alias for @Ungrowable @Unshrinkable @Unreplaceable @Unmodifiable List unmod = List.of("a", "b"); - unmod.add("c"); // Error: add() requires @Growable, got @UnknownGrow + unmod.add("c"); // Error: add() requires @Growable, got @Ungrowable unmod.get(0); // OK: read-only access needs no capability } @@ -380,4 +409,5 @@ % LocalWords: UnknownReplace growable ListIterator PolyModifiable addFirst % LocalWords: synchronizedList PolyShrink PolyGrow PolyReplace ThrowsUOE % LocalWords: addLast unreplaceable Growability shrinkability replacability -% LocalWords: growability Shrinkable Replaceable +% LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable +% LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace From 17afff17ab2fda3d1f50894ab7ffb96dba0305fe Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 19 Apr 2026 14:57:48 -0700 Subject: [PATCH 105/308] special annotation for CollectionsPlume.withoutDuplicates. --- .../ModifiabilityMethodUtils.java | 43 +++++++++++++++++++ .../grow/GrowAnnotatedTypeFactory.java | 22 ++++++++++ .../replace/ReplaceAnnotatedTypeFactory.java | 22 ++++++++++ .../shrink/ShrinkAnnotatedTypeFactory.java | 22 ++++++++++ 4 files changed, 109 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java new file mode 100644 index 000000000000..7164132e9c6d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java @@ -0,0 +1,43 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.MethodInvocationTree; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import org.checkerframework.javacutil.TreeUtils; + +/** Utility methods for Modifiability checker method-specific behavior. */ +public final class ModifiabilityMethodUtils { + + /** The fully-qualified class name of plume-util's collections utilities. */ + private static final String COLLECTIONS_PLUME = "org.plumelib.util.CollectionsPlume"; + + /** The method name for removing duplicates from a collection. */ + private static final String WITHOUT_DUPLICATES = "withoutDuplicates"; + + /** This is a utility class. */ + private ModifiabilityMethodUtils() { + throw new Error("Do not instantiate"); + } + + /** + * Returns true if {@code tree} invokes {@code org.plumelib.util.CollectionsPlume.withoutDuplicates(Collection)}. + * + * @param tree a method invocation tree + * @return true iff {@code tree} invokes {@code CollectionsPlume.withoutDuplicates(Collection)} + */ + public static boolean isCollectionsPlumeWithoutDuplicates(MethodInvocationTree tree) { + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null + || !invokedMethod.getSimpleName().contentEquals(WITHOUT_DUPLICATES) + || invokedMethod.getParameters().size() != 1) { + return false; + } + + Element enclosingElement = invokedMethod.getEnclosingElement(); + if (!(enclosingElement instanceof TypeElement enclosingType)) { + return false; + } + return enclosingType.getQualifiedName().contentEquals(COLLECTIONS_PLUME); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 5d8f449ad758..487f1eeb6348 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.modifiability.grow; +import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -7,6 +8,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomGrow; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; @@ -18,7 +20,9 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.QualifierUpperBounds; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; @@ -88,6 +92,24 @@ protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator(new GrowTypeAnnotator(this), super.createTypeAnnotator()); } + @Override + protected ParameterizedExecutableType methodFromUse( + MethodInvocationTree tree, boolean inferTypeArgs) { + ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { + return mType; + } + + AnnotatedExecutableType method = mType.executableType(); + AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); + if (argumentType.hasPrimaryAnnotation(GROWABLE)) { + method.getReturnType().replaceAnnotation(GROWABLE); + } else { + method.getReturnType().replaceAnnotation(UNKNOWN_GROW); + } + return mType; + } + /** * Removes capabilities that cannot be supported by structural constraints of the collection type: * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 2a1a7b42dfe1..371925c178f0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.modifiability.replace; +import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -7,6 +8,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplace; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; @@ -18,7 +20,9 @@ import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.QualifierUpperBounds; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; @@ -106,6 +110,24 @@ protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator(new ReplaceTypeAnnotator(this), super.createTypeAnnotator()); } + @Override + protected ParameterizedExecutableType methodFromUse( + MethodInvocationTree tree, boolean inferTypeArgs) { + ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { + return mType; + } + + AnnotatedExecutableType method = mType.executableType(); + AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); + if (argumentType.hasPrimaryAnnotation(REPLACEABLE)) { + method.getReturnType().replaceAnnotation(REPLACEABLE); + } else { + method.getReturnType().replaceAnnotation(UNKNOWN_REPLACE); + } + return mType; + } + /** * Removes capabilities that cannot be supported by structural constraints of the collection type: * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index d80d88cb9630..0178aba737cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.modifiability.shrink; +import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -7,6 +8,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; @@ -18,7 +20,9 @@ import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.QualifierUpperBounds; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; @@ -83,6 +87,24 @@ protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator(new ShrinkTypeAnnotator(this), super.createTypeAnnotator()); } + @Override + protected ParameterizedExecutableType methodFromUse( + MethodInvocationTree tree, boolean inferTypeArgs) { + ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { + return mType; + } + + AnnotatedExecutableType method = mType.executableType(); + AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); + if (argumentType.hasPrimaryAnnotation(SHRINKABLE)) { + method.getReturnType().replaceAnnotation(SHRINKABLE); + } else { + method.getReturnType().replaceAnnotation(UNKNOWN_SHRINK); + } + return mType; + } + /** * Removes capabilities that cannot be supported by structural constraints of the collection type: * From 27cc0751b400fb07231e08ca7f265b84b668d14a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 20 Apr 2026 13:10:49 -0700 Subject: [PATCH 106/308] Move annotation and method --- .../qual/DoesNotUnrefineReceiver.java | 2 +- docs/CHANGELOG.md | 4 ++-- .../framework/type/AnnotatedTypeFactory.java | 11 +++++++--- .../javacutil/AnnotationProvider.java | 8 -------- .../javacutil/BasicAnnotationProvider.java | 20 ------------------- 5 files changed, 11 insertions(+), 34 deletions(-) rename checker-qual/src/main/java/org/checkerframework/{dataflow => framework}/qual/DoesNotUnrefineReceiver.java (96%) diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/DoesNotUnrefineReceiver.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/DoesNotUnrefineReceiver.java index 329ea505d64e..4eca89c86484 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/DoesNotUnrefineReceiver.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/DoesNotUnrefineReceiver.java @@ -1,4 +1,4 @@ -package org.checkerframework.dataflow.qual; +package org.checkerframework.framework.qual; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7e7df90823b3..455db7fd657b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,8 +9,8 @@ ### Implementation details -New method annotation `DoesNotUnrefineReceiver`. -Interface `AnnotationProvider` has a new method `hasDoesNotUnrefineReceiver()`. +New method annotation `@DoesNotUnrefineReceiver`. +`AnnotatedTypeFactory` has a new method `hasDoesNotUnrefineReceiver()`. ### Closed issues diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 96df1705373d..a9bd6fe10847 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -96,9 +96,9 @@ import org.checkerframework.common.wholeprograminference.WholeProgramInferenceJavaParserStorage; import org.checkerframework.common.wholeprograminference.WholeProgramInferenceJavaParserStorage.InferredDeclared; import org.checkerframework.common.wholeprograminference.WholeProgramInferenceScenesStorage; -import org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver; import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.framework.qual.AnnotatedFor; +import org.checkerframework.framework.qual.DoesNotUnrefineReceiver; import org.checkerframework.framework.qual.EnsuresQualifier; import org.checkerframework.framework.qual.EnsuresQualifierIf; import org.checkerframework.framework.qual.FieldInvariant; @@ -5862,10 +5862,15 @@ public boolean isDeterministic(ExecutableElement methodElement) { return false; } - @Override + /** + * Returns true if the given method may have side effects but does not unrefine its receiver. + * + * @param methodElement a method + * @return true if the method does not unrefine its receiver + */ public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { for (AnnotationMirror am : getDeclAnnotations(methodElement)) { - if (areSameByClass(am, org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver.class)) { + if (areSameByClass(am, org.checkerframework.framework.qual.DoesNotUnrefineReceiver.class)) { List typeSystems = AnnotationUtils.getElementValueArray( am, doesNotUnrefineReceiverValueElement, String.class); diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java index 5e2b84a222d6..e71ba812dd0b 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationProvider.java @@ -56,12 +56,4 @@ public interface AnnotationProvider { * @return true if multiple calls to the method (with the same arguments) return the same value */ boolean isDeterministic(ExecutableElement methodElement); - - /** - * Returns true if the given method may have side effects but does not unrefine its receiver. - * - * @param methodElement a method - * @return true if the method does not unrefine its receiver - */ - boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement); } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java index 4fdb3b3fefe4..a160fd8a800c 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/BasicAnnotationProvider.java @@ -83,24 +83,4 @@ public boolean isDeterministic(ExecutableElement methodElement) { return false; } - - /** - * {@inheritDoc} - * - *

This implementation returns true if the {@code @DoesNotUnrefineReceiver} annotation is - * present on the given method, with "allcheckers" in the {@code value} element. - */ - @Override - public boolean hasDoesNotUnrefineReceiver(ExecutableElement methodElement) { - for (AnnotationMirror am : methodElement.getAnnotationMirrors()) { - if (AnnotationUtils.areSameByName( - am, "org.checkerframework.dataflow.qual.DoesNotUnrefineReceiver")) { - @SuppressWarnings("deprecation") // calls from the framework are permitted - List typeSystems = - AnnotationUtils.getElementValueArray(am, "value", String.class, true); - return typeSystems.contains("allcheckers"); - } - } - return false; - } } From 025c38771e01022653386fe1d57397407a0ed3ff Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 21 Apr 2026 17:14:13 -0700 Subject: [PATCH 107/308] updated circleci/config.yml. --- .circleci/config.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 97ef767375c4..94a05e843e8c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -502,11 +502,15 @@ workflows: - junit_part1_jdk25 - junit_part2_jdk25 - nonjunit_jdk25 + - inference_part1_jdk25 + - inference_part2_jdk25 - typecheck_part1_jdk25 - typecheck_part2_jdk25 - misc_jdk25 - misc_jdk26 + - junit_part1_jdk25 + - junit_part2_jdk25 - nonjunit_jdk25 - misc_jdk11: requires: @@ -524,9 +528,7 @@ workflows: - misc_jdk25 - misc_jdk25 - - misc_jdk26: - requires: - - canary_jobs + - misc_jdk26 - typecheck_part1_jdk25 - typecheck_part2_jdk25 @@ -535,13 +537,9 @@ workflows: requires: - canary_jobs - - inference_part1_jdk25: - requires: - - canary_jobs + - inference_part1_jdk25 - - inference_part2_jdk25: - requires: - - canary_jobs + - inference_part2_jdk25 - daikon_part1_jdk25: requires: From 19d63dda4c9b63d7d7bb203391deb1731b9ba30d Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 21 Apr 2026 17:26:32 -0700 Subject: [PATCH 108/308] added ci.yml in github to enable github action --- .github/workflows/ci.yml | 319 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..99444f47513e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,319 @@ +name: CI + +on: + push: + branches: + - "**" + pull_request: + branches: + - "**" + +permissions: + contents: read + +jobs: + junit_part1_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-junit.sh part1 + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-cftests-junit.sh part1 + + junit_part2_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-junit.sh part2 + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-cftests-junit.sh part2 + + nonjunit_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-nonjunit.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-cftests-nonjunit.sh + + inference_part1_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-inference-part1.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-cftests-inference-part1.sh + + inference_part2_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-inference-part2.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-cftests-inference-part2.sh + + typecheck_part1_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1000 + - name: test-typecheck-part1.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-typecheck-part1.sh + + typecheck_part2_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1000 + - name: test-typecheck-part2.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-typecheck-part2.sh + + misc_jdk25: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25-plus:latest + steps: + - uses: actions/checkout@v4 + with: + # Unlimited history for contributors.tex generation. + fetch-depth: 0 + - name: test-misc.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-misc.sh + + misc_jdk26: + runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk26-plus:latest + steps: + - uses: actions/checkout@v4 + with: + # Unlimited history for contributors.tex generation. + fetch-depth: 0 + - name: test-misc.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "26" + run: ./checker/bin-devel/test-misc.sh + + canary_jobs: + runs-on: ubuntu-latest + needs: + - junit_part1_jdk25 + - junit_part2_jdk25 + - nonjunit_jdk25 + - inference_part1_jdk25 + - inference_part2_jdk25 + - typecheck_part1_jdk25 + - typecheck_part2_jdk25 + - misc_jdk25 + - misc_jdk26 + steps: + - name: canary_jobs + run: true + + junit_jdk17: + runs-on: ubuntu-latest + needs: + - canary_jobs + - junit_part1_jdk25 + - junit_part2_jdk25 + container: + image: mdernst/cf-ubuntu-jdk17:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-junit.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "17" + run: ./checker/bin-devel/test-cftests-junit.sh + + junit_jdk21: + runs-on: ubuntu-latest + needs: + - canary_jobs + - junit_part1_jdk25 + - junit_part2_jdk25 + container: + image: mdernst/cf-ubuntu-jdk21:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-junit.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "21" + run: ./checker/bin-devel/test-cftests-junit.sh + + junit_jdk26: + runs-on: ubuntu-latest + needs: + - canary_jobs + - junit_part1_jdk25 + - junit_part2_jdk25 + container: + image: mdernst/cf-ubuntu-jdk26:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-cftests-junit.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "26" + run: ./checker/bin-devel/test-cftests-junit.sh + + misc_jdk17: + runs-on: ubuntu-latest + needs: + - canary_jobs + - misc_jdk25 + container: + image: mdernst/cf-ubuntu-jdk17-plus:latest + steps: + - uses: actions/checkout@v4 + with: + # Unlimited history for contributors.tex generation. + fetch-depth: 0 + - name: test-misc.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "17" + run: ./checker/bin-devel/test-misc.sh + + misc_jdk21: + runs-on: ubuntu-latest + needs: + - canary_jobs + - misc_jdk25 + container: + image: mdernst/cf-ubuntu-jdk21-plus:latest + steps: + - uses: actions/checkout@v4 + with: + # Unlimited history for contributors.tex generation. + fetch-depth: 0 + - name: test-misc.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "21" + run: ./checker/bin-devel/test-misc.sh + + daikon_part1_jdk25: + runs-on: ubuntu-latest + needs: + - canary_jobs + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-daikon-part1.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-daikon-part1.sh + + daikon_part2_jdk25: + runs-on: ubuntu-latest + needs: + - canary_jobs + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 80 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-daikon-part2.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-daikon-part2.sh + + daikon_part3_jdk25: + runs-on: ubuntu-latest + needs: + - canary_jobs + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 80 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-daikon-part3.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-daikon-part3.sh + + guava_jdk25: + runs-on: ubuntu-latest + needs: + - canary_jobs + container: + image: mdernst/cf-ubuntu-jdk25:latest + timeout-minutes: 70 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-guava.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-guava.sh + + plume_lib_jdk25: + runs-on: ubuntu-latest + needs: + - canary_jobs + container: + image: mdernst/cf-ubuntu-jdk25:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 25 + - name: test-plume-lib.sh + env: + ORG_GRADLE_PROJECT_jdkTestVersion: "25" + run: ./checker/bin-devel/test-plume-lib.sh From 75ff6c08ae8c95a003c6ec9cf0518ae2b9ea558f Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 21 Apr 2026 17:48:39 -0700 Subject: [PATCH 109/308] update ci.yml. --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99444f47513e..0d92776ff6fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,11 @@ on: permissions: contents: read +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: ${{ github.workspace }} + jobs: junit_part1_jdk25: runs-on: ubuntu-latest @@ -20,6 +25,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh part1 env: @@ -34,6 +40,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh part2 env: @@ -47,6 +54,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-nonjunit.sh env: @@ -61,6 +69,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-inference-part1.sh env: @@ -75,6 +84,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-inference-part2.sh env: @@ -88,6 +98,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 1000 - name: test-typecheck-part1.sh env: @@ -101,6 +112,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 1000 - name: test-typecheck-part2.sh env: @@ -114,6 +126,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -128,6 +141,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -163,6 +177,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -181,6 +196,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -199,6 +215,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -215,6 +232,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -232,6 +250,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -249,6 +268,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-daikon-part1.sh env: @@ -265,6 +285,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-daikon-part2.sh env: @@ -281,6 +302,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-daikon-part3.sh env: @@ -297,6 +319,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-guava.sh env: @@ -312,6 +335,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-plume-lib.sh env: From 421bc14fd476618f87dfa45e0b93d624fcd30e83 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 21 Apr 2026 22:18:08 -0700 Subject: [PATCH 110/308] remove safe checks in ci.yml --- .github/workflows/ci.yml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d92776ff6fc..47d6d56a1cd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,14 +8,6 @@ on: branches: - "**" -permissions: - contents: read - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: safe.directory - GIT_CONFIG_VALUE_0: ${{ github.workspace }} - jobs: junit_part1_jdk25: runs-on: ubuntu-latest @@ -25,7 +17,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh part1 env: @@ -40,7 +31,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh part2 env: @@ -54,7 +44,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-nonjunit.sh env: @@ -69,7 +58,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-inference-part1.sh env: @@ -84,7 +72,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-inference-part2.sh env: @@ -98,7 +85,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 1000 - name: test-typecheck-part1.sh env: @@ -112,7 +98,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 1000 - name: test-typecheck-part2.sh env: @@ -126,7 +111,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -141,7 +125,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -177,7 +160,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -196,7 +178,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -215,7 +196,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -232,7 +212,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -250,7 +229,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -268,7 +246,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-daikon-part1.sh env: @@ -285,7 +262,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-daikon-part2.sh env: @@ -302,7 +278,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-daikon-part3.sh env: @@ -319,7 +294,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-guava.sh env: @@ -335,7 +309,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - set-safe-directory: true fetch-depth: 25 - name: test-plume-lib.sh env: From b2c692573dceac04fa2d9cccca214bf7094006bb Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 21 Apr 2026 22:58:20 -0700 Subject: [PATCH 111/308] ci build failed, add back safe.directory. --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47d6d56a1cd3..10f4da45688a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,12 @@ on: branches: - "**" +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: ${{ github.workspace }} + + jobs: junit_part1_jdk25: runs-on: ubuntu-latest @@ -17,6 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh part1 env: @@ -31,6 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh part2 env: @@ -44,6 +52,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-nonjunit.sh env: @@ -58,6 +67,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-inference-part1.sh env: @@ -72,6 +82,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-inference-part2.sh env: @@ -85,6 +96,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 1000 - name: test-typecheck-part1.sh env: @@ -98,6 +110,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 1000 - name: test-typecheck-part2.sh env: @@ -111,6 +124,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -125,6 +139,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -160,6 +175,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -178,6 +194,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -196,6 +213,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-cftests-junit.sh env: @@ -212,6 +230,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -229,6 +248,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: test-misc.sh @@ -246,6 +266,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-daikon-part1.sh env: @@ -262,6 +283,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-daikon-part2.sh env: @@ -278,6 +300,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-daikon-part3.sh env: @@ -294,6 +317,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-guava.sh env: @@ -309,6 +333,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + set-safe-directory: true fetch-depth: 25 - name: test-plume-lib.sh env: From 3c64a429df68e9a0893bf41c358d7008fc326ec1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 22 Apr 2026 17:46:34 -0700 Subject: [PATCH 112/308] replicate [conflicting.annos] error in randoop. ./gradlew ModifiabilityTest is throwing this error, but if we run the files alone, the error is gone. Co-authored-by: Copilot --- .../IteratorConflictingAnnosTypeTuple.java | 13 +++++++++++++ .../IteratorConflictingAnnosUse.java | 15 +++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java create mode 100644 checker/tests/modifiability/IteratorConflictingAnnosUse.java diff --git a/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java b/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java new file mode 100644 index 000000000000..c5978c410158 --- /dev/null +++ b/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java @@ -0,0 +1,13 @@ +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +class IteratorConflictingAnnosTypeTuple implements Iterable { + private final ArrayList list = new ArrayList<>(); + + @Override + public @Unmodifiable Iterator iterator(IteratorConflictingAnnosTypeTuple this) { + return Collections.unmodifiableList(list).iterator(); + } +} diff --git a/checker/tests/modifiability/IteratorConflictingAnnosUse.java b/checker/tests/modifiability/IteratorConflictingAnnosUse.java new file mode 100644 index 000000000000..3e5f5c07daf5 --- /dev/null +++ b/checker/tests/modifiability/IteratorConflictingAnnosUse.java @@ -0,0 +1,15 @@ +import java.util.Iterator; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +// line 12 is throwing an error +// IteratorConflictingAnnosUse.java:12: error: [conflicting.annos] +// when running modifiabilityTest, but this error is not thrown when running this file alone. +public class IteratorConflictingAnnosUse { + private final IteratorConflictingAnnosTypeTuple inputTypes = + new IteratorConflictingAnnosTypeTuple(); + + void reproduce() { + @Unmodifiable Iterator inputTypeIterator = inputTypes.iterator(); + inputTypeIterator.hasNext(); + } +} From 617e8aa918151cfbc22db0defc20394ebd86f2de Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 23 Apr 2026 00:22:01 -0700 Subject: [PATCH 113/308] implemented the logic for iterators, only annotated arrayLsit and map for testing. currently IteratorPreserveRemove is not dataflowable, not sure why. Co-authored-by: Copilot --- .../qual/IteratorPreserveRemove.java | 20 ++++ .../shrink/ShrinkAnnotatedTypeFactory.java | 102 +++++++++++++++++- .../modifiability/IteratorPrecisionTest.java | 63 +++++++++++ 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java create mode 100644 checker/tests/modifiability/IteratorPrecisionTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java new file mode 100644 index 000000000000..e064385da247 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Marker annotation for collection-like types whose {@code iterator()} result should preserve the + * ability to call {@code Iterator.remove()}. + * + *

This annotation is intentionally not part of any qualifier hierarchy. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface IteratorPreserveRemove {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index b819cbba6f36..ab673c1969a8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -1,15 +1,18 @@ package org.checkerframework.checker.modifiability.shrink; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrink; +import org.checkerframework.checker.modifiability.qual.IteratorPreserveRemove; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; @@ -20,6 +23,7 @@ import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -28,6 +32,8 @@ import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The type factory for the Modifiability Checker. */ @@ -36,6 +42,9 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryErasure; + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; + // ── Hierarchy qualifiers ────────── /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ @@ -50,6 +59,9 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyShrink} qualifier. */ private AnnotationMirror POLY_SHRINK; + /** The {@code @}{@link IteratorPreserveRemove} marker annotation. */ + private AnnotationMirror ITERATOR_PRESERVE_REMOVE; + /** * Creates a ShrinkAnnotatedTypeFactory. * @@ -62,12 +74,16 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { Types types = getProcessingEnv().getTypeUtils(); this.mapEntryErasure = types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); + this.iteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. this.UNKNOWN_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), UnknownShrink.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); + this.ITERATOR_PRESERVE_REMOVE = + AnnotationBuilder.fromClass(getElementUtils(), IteratorPreserveRemove.class); addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNSHRINKABLE); @@ -96,11 +112,16 @@ protected TypeAnnotator createTypeAnnotator() { protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + AnnotatedExecutableType method = mType.executableType(); + + if (isIteratorMethod(tree, method)) { + refineIteratorReturnType(tree, method); + } + if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { return mType; } - AnnotatedExecutableType method = mType.executableType(); AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(SHRINKABLE)) { method.getReturnType().replaceAnnotation(SHRINKABLE); @@ -110,12 +131,89 @@ protected ParameterizedExecutableType methodFromUse( return mType; } + /** + * Refines {@code iterator()} return type based on {@code @IteratorPreserveRemove}. + * + *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked + * method receiver type has {@code @IteratorPreserveRemove}, then the result is {@code @Shrinkable + * Iterator}. Otherwise, shrinkability precision is dropped to {@code @UnknownShrink}. + */ + private void refineIteratorReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + AnnotatedTypeMirror returnType = methodType.getReturnType(); + if (returnType.hasPrimaryAnnotation(UNSHRINKABLE) + || returnType.hasPrimaryAnnotation(SHRINKABLE)) { + // Keep explicit unshrinkable iterator contracts (for example, CopyOnWriteArrayList). + return; + } + + Tree receiverTree = TreeUtils.getReceiverTree(tree); + if (receiverTree == null) { + return; + } + AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + + if (receiverType.hasPrimaryAnnotation(UNSHRINKABLE)) { + returnType.replaceAnnotation(UNSHRINKABLE); + } + + if (!receiverType.hasPrimaryAnnotation(SHRINKABLE)) { + return; + } + + // receiver type is @Shrinkable. check for @IteratorPreserveRemove + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + boolean receiverTreeHasMarker = + AnnotationUtils.containsSameByClass( + TreeUtils.typeOf(receiverTree).getAnnotationMirrors(), IteratorPreserveRemove.class); + AnnotatedTypeMirror methodReceiverType = methodType.getReceiverType(); + boolean methodElementReceiverHasMarker = + invokedMethod != null + && AnnotationUtils.containsSameByClass( + invokedMethod.getReceiverType().getAnnotationMirrors(), + IteratorPreserveRemove.class); + boolean preserveRemove = + hasIteratorPreserveRemove(receiverType) + || receiverTreeHasMarker + || (methodReceiverType != null && hasIteratorPreserveRemove(methodReceiverType)) + || methodElementReceiverHasMarker; + + if (preserveRemove) { + returnType.replaceAnnotation(SHRINKABLE); + } else { + returnType.replaceAnnotation(UNKNOWN_SHRINK); + } + } + + /** + * Returns true if this invocation is an instance {@code iterator()} method returning Iterator. + */ + private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (!tree.getArguments().isEmpty()) { + return false; + } + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null || !invokedMethod.getSimpleName().contentEquals("iterator")) { + return false; + } + TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); + return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); + } + + /** Returns true if {@code type} has the {@code @IteratorPreserveRemove} marker annotation. */ + private boolean hasIteratorPreserveRemove(AnnotatedTypeMirror type) { + if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + return true; + } + return AnnotationUtils.containsSameByClass( + type.getUnderlyingType().getAnnotationMirrors(), IteratorPreserveRemove.class); + } + /** * Removes capabilities that cannot be supported by structural constraints of the collection type: * *

    *
  • Map.Entry: remove Shrink capabilities - *
  • Iterator: remove Shrink capabilities *
*/ private class ShrinkTypeAnnotator extends TypeAnnotator { diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java new file mode 100644 index 000000000000..92f5f2ed9add --- /dev/null +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -0,0 +1,63 @@ +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import org.checkerframework.checker.modifiability.qual.IteratorPreserveRemove; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; + +/** Reproduces precision loss through an unannotated Set local from Map.keySet(). */ +public class IteratorPrecisionTest { + + void arrayListIterator() { + // ok + ArrayList list = new ArrayList<>(); + @Shrinkable Iterator iterator = list.iterator(); + iterator.remove(); + + // should still be ok, but is throwing error. + @IteratorPreserveRemove List list2 = new ArrayList<>(); + @Shrinkable Iterator iterator2 = list2.iterator(); + iterator2.remove(); + } + + void copyOnWriteArrayListIterator() { + // as expected, + CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); + @Unshrinkable Iterator iterator = list.iterator(); + // :: error: [method.invocation] + iterator.remove(); + + // throw error. + List list2 = new CopyOnWriteArrayList<>(); + // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: + // if the current iterator return unknown (which is what List.iterator() returns), + // then if the receiver is @Shrinkable and has @IteratorPreserveRemove, then the result is + // @Shrinkable. + // otherwise, the result is unknown. + // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? + Iterator iterator2 = list2.iterator(); + // :: error: [method.invocation] + iterator2.remove(); + } + + void KeySetIterator() { + @Modifiable Map map = new LinkedHashMap<>(); + map.put("a", "1"); + map.put("b", "2"); + + // should be ok, but is throwing error. + map.keySet().iterator().remove(); + + // ok + @IteratorPreserveRemove + @Shrinkable Set keys = map.keySet(); + + // The iterator supports remove(), but the type is treated as unknown shrinkability. + @Shrinkable Iterator lost = keys.iterator(); + } +} From b4eb009c96698bb8f8dab04d93d5e80d1c112545 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 23 Apr 2026 15:23:45 -0700 Subject: [PATCH 114/308] added UnknownIter. add both qualifier to the shrinkAnnotatedTypeFactory. edit test case. TODO: double check the logic in refining iterator function. Co-authored-by: Copilot --- .../qual/IteratorPreserveRemove.java | 6 ++-- .../modifiability/qual/UnknownIter.java | 25 +++++++++++++++++ .../shrink/ShrinkAnnotatedTypeFactory.java | 5 +++- .../modifiability/IteratorPrecisionTest.java | 28 +++++++++++-------- 4 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java index e064385da247..c39c25d90131 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java @@ -5,16 +5,16 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; /** - * Marker annotation for collection-like types whose {@code iterator()} result should preserve the + * Collections in the Java Collection Framework whose {@code iterator()} result preserves the * ability to call {@code Iterator.remove()}. * - *

This annotation is intentionally not part of any qualifier hierarchy. - * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownIter.class) public @interface IteratorPreserveRemove {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java new file mode 100644 index 000000000000..fa9f70062d27 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.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 top qualifier in the iterator-remove-preservation hierarchy. Represents unknown behavior + * about whether calling {@code iterator()} preserves the ability to call {@code Iterator.remove()}. + * + *

This is the default qualifier for unannotated types in the iterator-remove-preservation + * hierarchy. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface UnknownIter {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index ab673c1969a8..30f42cf4ab54 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -17,6 +17,7 @@ import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnknownIter; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -100,7 +101,9 @@ protected Set> createSupportedTypeQualifiers() { Shrinkable.class, Unshrinkable.class, BottomShrink.class, - PolyShrink.class)); + PolyShrink.class, + UnknownIter.class, + IteratorPreserveRemove.class)); } @Override diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 92f5f2ed9add..d067860ac830 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; -import org.checkerframework.checker.modifiability.qual.IteratorPreserveRemove; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; @@ -19,9 +18,9 @@ void arrayListIterator() { @Shrinkable Iterator iterator = list.iterator(); iterator.remove(); - // should still be ok, but is throwing error. - @IteratorPreserveRemove List list2 = new ArrayList<>(); - @Shrinkable Iterator iterator2 = list2.iterator(); + // should still be ok + List list2 = new ArrayList<>(); + Iterator iterator2 = list2.iterator(); iterator2.remove(); } @@ -29,8 +28,6 @@ void copyOnWriteArrayListIterator() { // as expected, CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); @Unshrinkable Iterator iterator = list.iterator(); - // :: error: [method.invocation] - iterator.remove(); // throw error. List list2 = new CopyOnWriteArrayList<>(); @@ -46,18 +43,25 @@ void copyOnWriteArrayListIterator() { } void KeySetIterator() { - @Modifiable Map map = new LinkedHashMap<>(); + LinkedHashMap map = new LinkedHashMap<>(); map.put("a", "1"); map.put("b", "2"); - - // should be ok, but is throwing error. map.keySet().iterator().remove(); - // ok - @IteratorPreserveRemove @Shrinkable Set keys = map.keySet(); + @Shrinkable Iterator iterator = keys.iterator(); + + @Modifiable Map map2 = new LinkedHashMap<>(); + map2.put("a", "1"); + map2.put("b", "2"); + + // should be ok + map2.keySet().iterator().remove(); + + // ok + @Shrinkable Set keys2 = map2.keySet(); // The iterator supports remove(), but the type is treated as unknown shrinkability. - @Shrinkable Iterator lost = keys.iterator(); + @Shrinkable Iterator lost = keys2.iterator(); } } From 393db9e98d9613889b1844cadf0ef24ab3662f4f Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 24 Apr 2026 15:16:12 -0700 Subject: [PATCH 115/308] refine logic in shrinkAnnotatedTypeFactory, add a case in test. --- .../shrink/ShrinkAnnotatedTypeFactory.java | 42 +++++++------------ .../modifiability/IteratorPrecisionTest.java | 8 +++- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 30f42cf4ab54..70e341bc1bba 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -60,7 +60,10 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyShrink} qualifier. */ private AnnotationMirror POLY_SHRINK; - /** The {@code @}{@link IteratorPreserveRemove} marker annotation. */ + /** The {@code @}{@link UnknownIter} qualifier (top of iterator-preservation hierarchy). */ + private AnnotationMirror UNKNOWN_ITER; + + /** The {@code @}{@link IteratorPreserveRemove} qualifier. */ private AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -83,6 +86,7 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); + this.UNKNOWN_ITER = AnnotationBuilder.fromClass(getElementUtils(), UnknownIter.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPreserveRemove.class); @@ -144,9 +148,11 @@ protected ParameterizedExecutableType methodFromUse( private void refineIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror returnType = methodType.getReturnType(); + // Keep explicit unshrinkable/shrinkable iterator contracts (for example, CopyOnWriteArrayList, + // ArrayList). if (returnType.hasPrimaryAnnotation(UNSHRINKABLE) - || returnType.hasPrimaryAnnotation(SHRINKABLE)) { - // Keep explicit unshrinkable iterator contracts (for example, CopyOnWriteArrayList). + || returnType.hasPrimaryAnnotation(SHRINKABLE) + || returnType.hasPrimaryAnnotation(POLY_SHRINK)) { return; } @@ -156,6 +162,7 @@ private void refineIteratorReturnType( } AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + // all unshrinkable collections' iterators are unshrinkable. if (receiverType.hasPrimaryAnnotation(UNSHRINKABLE)) { returnType.replaceAnnotation(UNSHRINKABLE); } @@ -165,23 +172,7 @@ private void refineIteratorReturnType( } // receiver type is @Shrinkable. check for @IteratorPreserveRemove - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - boolean receiverTreeHasMarker = - AnnotationUtils.containsSameByClass( - TreeUtils.typeOf(receiverTree).getAnnotationMirrors(), IteratorPreserveRemove.class); - AnnotatedTypeMirror methodReceiverType = methodType.getReceiverType(); - boolean methodElementReceiverHasMarker = - invokedMethod != null - && AnnotationUtils.containsSameByClass( - invokedMethod.getReceiverType().getAnnotationMirrors(), - IteratorPreserveRemove.class); - boolean preserveRemove = - hasIteratorPreserveRemove(receiverType) - || receiverTreeHasMarker - || (methodReceiverType != null && hasIteratorPreserveRemove(methodReceiverType)) - || methodElementReceiverHasMarker; - - if (preserveRemove) { + if (hasIteratorPreserveRemove(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { returnType.replaceAnnotation(UNKNOWN_SHRINK); @@ -253,15 +244,14 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { @Override protected QualifierUpperBounds createQualifierUpperBounds() { return new QualifierUpperBounds(this) { - private final AnnotationMirrorSet unknownShrink = - AnnotationMirrorSet.singleton(UNKNOWN_SHRINK); - @Override public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { if (TypesUtils.isErasedSubtype(type, mapEntryErasure, types)) { - // Elements of a map entry can never be shrunk, so treat them as @UnknownShrink. Even if - // they are annotation @Growable in a stubfile. - return unknownShrink; + // Map.Entry uses fixed upper bounds in both supported hierarchies. + AnnotationMirrorSet bounds = new AnnotationMirrorSet(); + bounds.add(UNKNOWN_SHRINK); + bounds.add(UNKNOWN_ITER); + return bounds; } return super.getBoundQualifiers(type); } diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index d067860ac830..894ffe01a4f5 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -9,7 +9,6 @@ import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; -/** Reproduces precision loss through an unannotated Set local from Map.keySet(). */ public class IteratorPrecisionTest { void arrayListIterator() { @@ -42,6 +41,13 @@ void copyOnWriteArrayListIterator() { iterator2.remove(); } + void UnmodListIterator() { + List list = List.of("a", "b"); + @Unshrinkable Iterator iterator = list.iterator(); + // :: error: [method.invocation] + iterator.remove(); + } + void KeySetIterator() { LinkedHashMap map = new LinkedHashMap<>(); map.put("a", "1"); From 34e9922796873a85be5efe77caae42f070c66fb5 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 26 Apr 2026 14:47:49 -0700 Subject: [PATCH 116/308] add iteratorPreserveRemove to the manual --- docs/manual/modifiability-checker.tex | 44 ++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 20a3a71a4996..67a57ff26376 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -63,20 +63,22 @@ The Modifiability Checker uses \textbf{three independent 4-element hierarchies}, one per capability. Every annotated type carries exactly -one qualifier from each hierarchy. +one qualifier from each hierarchy. The checker also uses a fourth, +2-element hierarchy to express whether a collection's \code{iterator} + preserves the ability to call \code{Iterator.remove()}. % TODO: add a figure that follows the sturucture of other checker manuals. \begin{figure} \centering \begin{minipage}{0.85\linewidth} \begin{verbatim} - Grow hierarchy Shrink hierarchy Replace hierarchy + Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace - / \ / \ / \ - @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable - \ / \ / \ / - @BottomGrow @BottomShrink @BottomReplace + @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIter + / \ / \ / \ | + @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | + \ / \ / \ / | + @BottomGrow @BottomShrink @BottomReplace @IteratorPreserveRemove \end{verbatim} \end{minipage} \caption{The three independent Modifiability qualifier hierarchies. @@ -160,6 +162,19 @@ The bottom qualifier in the Replace hierarchy. It is a subtype of both \<@Replaceable> and \<@Unreplaceable>. Programmers should rarely write it. +\item[\refqualclass{checker/modifiability/qual}{UnknownIter}] +The top qualifier in the iterator-remove-preservation hierarchy. +The checker cannot determine whether calling \code{iterator()} on this +collection preserves the ability to call \code{Iterator.remove()}. +This is the default qualifier for unannotated types in the +iterator-remove-preservation hierarchy. + +\item[\refqualclass{checker/modifiability/qual}{IteratorPreserveRemove}] +Calling \code{iterator()} on this collection preserves the receiver's +ability to remove elements: if the collection is \<@Shrinkable>, then +the resulting \code{Iterator} is \<@Shrinkable> and its +\code{remove} method may be called. + \end{description} \subsectionAndLabel{Combining capabilities}{modifiability-combining} @@ -268,6 +283,20 @@ treated as \code{@UnknownGrow @UnknownShrink @UnknownReplace Iterator} \end{itemize} +The iterator returned by a shrinkable collection does not always support +\code{Iterator.remove()}. Therefore, when \code{iterator()} is called on +an \<@Shrinkable> receiver, the result is \<@Shrinkable> only if the +receiver is also \<@IteratorPreserveRemove>. Otherwise, the result's +Shrink qualifier is conservatively \<@UnknownShrink>. When +\code{iterator()} is called on an \<@Unshrinkable> receiver, the result +is \<@Unshrinkable>. + +\begin{Verbatim} + @IteratorPreserveRemove @Shrinkable List list = ...; + @Shrinkable Iterator it = list.iterator(); + it.remove(); // OK +\end{Verbatim} + \subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} @@ -411,3 +440,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace +% LocalWords: UnknownIter IteratorPreserveRemove From 28a16c5ecb7b0d9372db7d480a99239f51eb15c9 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 26 Apr 2026 16:19:43 -0700 Subject: [PATCH 117/308] annotate modifiable collections as @IteratorPreserveRemove; add tests --- .../modifiability/IteratorPrecisionTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 894ffe01a4f5..348cdbdf9086 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -1,16 +1,36 @@ +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; public class IteratorPrecisionTest { + enum TestEnum { + A, + B + } + void arrayListIterator() { // ok ArrayList list = new ArrayList<>(); @@ -70,4 +90,57 @@ void KeySetIterator() { // The iterator supports remove(), but the type is treated as unknown shrinkability. @Shrinkable Iterator lost = keys2.iterator(); } + + void setIteratorPreservesRemove() { + Set hashSet = new HashSet<>(); + @Shrinkable Iterator hashSetIterator = hashSet.iterator(); + hashSetIterator.remove(); + + Set treeSet = new TreeSet<>(); + @Shrinkable Iterator treeSetIterator = treeSet.iterator(); + treeSetIterator.remove(); + } + + void dequeIteratorPreservesRemove() { + Deque arrayDeque = new ArrayDeque<>(); + @Shrinkable Iterator arrayDequeIterator = arrayDeque.iterator(); + arrayDequeIterator.remove(); + + Deque linkedList = new LinkedList<>(); + @Shrinkable Iterator linkedListIterator = linkedList.iterator(); + linkedListIterator.remove(); + } + + void queueIteratorPreservesRemove() { + Queue priorityQueue = new PriorityQueue<>(); + @Shrinkable Iterator priorityQueueIterator = priorityQueue.iterator(); + priorityQueueIterator.remove(); + + BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(1); + @Shrinkable Iterator arrayBlockingQueueIterator = arrayBlockingQueue.iterator(); + arrayBlockingQueueIterator.remove(); + + BlockingQueue linkedBlockingQueue = new LinkedBlockingQueue<>(); + @Shrinkable Iterator linkedBlockingQueueIterator = linkedBlockingQueue.iterator(); + linkedBlockingQueueIterator.remove(); + + TransferQueue linkedTransferQueue = new LinkedTransferQueue<>(); + @Shrinkable Iterator linkedTransferQueueIterator = linkedTransferQueue.iterator(); + linkedTransferQueueIterator.remove(); + } + + void factoryIteratorPreservesRemove() { + Set enumSet = EnumSet.of(TestEnum.A, TestEnum.B); + @Shrinkable Iterator enumSetIterator = enumSet.iterator(); + enumSetIterator.remove(); + + Set enumSetCopy = EnumSet.copyOf(enumSet); + @Shrinkable Iterator enumSetCopyIterator = enumSetCopy.iterator(); + enumSetCopyIterator.remove(); + + Enumeration enumeration = Collections.enumeration(List.of("a", "b")); + List list = Collections.list(enumeration); + @Shrinkable Iterator listIterator = list.iterator(); + listIterator.remove(); + } } From ac534d648ab6510f7822dba4540ab75cc5d289bd Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 26 Apr 2026 17:46:31 -0700 Subject: [PATCH 118/308] finished annotating map.keyset, values, and entryset. added more test. --- .../modifiability/IteratorPrecisionTest.java | 75 +++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 348cdbdf9086..0338898e16b0 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -2,8 +2,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Deque; +import java.util.EnumMap; import java.util.EnumSet; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -16,12 +18,15 @@ import java.util.TreeSet; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; public class IteratorPrecisionTest { @@ -48,7 +53,7 @@ void copyOnWriteArrayListIterator() { CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); @Unshrinkable Iterator iterator = list.iterator(); - // throw error. + // store CopyOnWriteArrayList as a list, the iterator is @UnknownShrink. List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), @@ -64,18 +69,17 @@ void copyOnWriteArrayListIterator() { void UnmodListIterator() { List list = List.of("a", "b"); @Unshrinkable Iterator iterator = list.iterator(); - // :: error: [method.invocation] - iterator.remove(); } void KeySetIterator() { LinkedHashMap map = new LinkedHashMap<>(); map.put("a", "1"); map.put("b", "2"); + // ok map.keySet().iterator().remove(); @Shrinkable Set keys = map.keySet(); - @Shrinkable Iterator iterator = keys.iterator(); + @Shrinkable Iterator iterator2 = keys.iterator(); @Modifiable Map map2 = new LinkedHashMap<>(); map2.put("a", "1"); @@ -86,9 +90,7 @@ void KeySetIterator() { // ok @Shrinkable Set keys2 = map2.keySet(); - - // The iterator supports remove(), but the type is treated as unknown shrinkability. - @Shrinkable Iterator lost = keys2.iterator(); + @Shrinkable Iterator iter2 = keys2.iterator(); } void setIteratorPreservesRemove() { @@ -143,4 +145,63 @@ void factoryIteratorPreservesRemove() { @Shrinkable Iterator listIterator = list.iterator(); listIterator.remove(); } + + void mapViewIteratorsPreserveRemove() { + HashMap hashMap = new HashMap<>(); + @Shrinkable Iterator hashMapKeys = hashMap.keySet().iterator(); + hashMapKeys.remove(); + @Shrinkable Iterator hashMapValues = hashMap.values().iterator(); + hashMapValues.remove(); + @Shrinkable Iterator> hashMapEntries = hashMap.entrySet().iterator(); + hashMapEntries.remove(); + + LinkedHashMap linkedHashMap = new LinkedHashMap<>(); + @Shrinkable Iterator linkedKeys = linkedHashMap.keySet().iterator(); + linkedKeys.remove(); + @Shrinkable Iterator linkedValues = linkedHashMap.values().iterator(); + linkedValues.remove(); + @Shrinkable Iterator> linkedEntries = linkedHashMap.entrySet().iterator(); + linkedEntries.remove(); + + EnumMap enumMap = new EnumMap<>(TestEnum.class); + @Shrinkable Iterator enumKeys = enumMap.keySet().iterator(); + enumKeys.remove(); + @Shrinkable Iterator enumValues = enumMap.values().iterator(); + enumValues.remove(); + @Shrinkable Iterator> enumEntries = enumMap.entrySet().iterator(); + enumEntries.remove(); + + ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap<>(); + @Shrinkable Iterator concurrentKeys = concurrentHashMap.keySet().iterator(); + concurrentKeys.remove(); + @Shrinkable Iterator concurrentValues = concurrentHashMap.values().iterator(); + concurrentValues.remove(); + @Shrinkable Iterator> concurrentEntries = + concurrentHashMap.entrySet().iterator(); + concurrentEntries.remove(); + + ConcurrentSkipListMap skipListMap = new ConcurrentSkipListMap<>(); + @Shrinkable Iterator skipListKeys = skipListMap.keySet().iterator(); + skipListKeys.remove(); + @Shrinkable Iterator skipListValues = skipListMap.values().iterator(); + skipListValues.remove(); + @Shrinkable Iterator> skipListEntries = + skipListMap.entrySet().iterator(); + skipListEntries.remove(); + } + + void unmodifiableMapViewIteratorsDoNotPreserveRemove() { + @Unmodifiable Map map = Map.of("a", "b"); + @Unshrinkable Iterator keyIterator = map.keySet().iterator(); + @Unshrinkable Iterator valueIterator = map.values().iterator(); + @Unshrinkable Iterator> entryIterator = map.entrySet().iterator(); + } + + void unmodifiableMapWrapperViewIteratorsDoNotPreserveRemove() { + HashMap backing = new HashMap<>(); + @Unmodifiable Map map = Collections.unmodifiableMap(backing); + @Unshrinkable Iterator keyIterator = map.keySet().iterator(); + @Unshrinkable Iterator valueIterator = map.values().iterator(); + @Unshrinkable Iterator> entryIterator = map.entrySet().iterator(); + } } From 5fda4e52ca169463e3cece85f5055f27cc5531c2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 27 Apr 2026 14:14:01 -0700 Subject: [PATCH 119/308] add test for iterator assignment --- checker/tests/modifiability/IteratorPrecisionTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 0338898e16b0..589832146b34 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -24,6 +24,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; +import org.checkerframework.checker.modifiability.qual.IteratorPreserveRemove; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -36,6 +37,15 @@ enum TestEnum { B } + @IteratorPreserveRemove List list; + + void iteratorChecker() { + list = + new CopyOnWriteArrayList<>(); // this should not be allowed because CopyOnWriteArrayList's + // iterator does not preserve remove + @Shrinkable Iterator iterator = list.iterator(); + } + void arrayListIterator() { // ok ArrayList list = new ArrayList<>(); From faa4f637e6ae8d4f4e9839a50d7e37c1b87aca87 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 28 Apr 2026 04:44:43 -0700 Subject: [PATCH 120/308] Rename variables --- .../framework/type/AnnotatedTypeFactory.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index a9bd6fe10847..9b7ca74fb72a 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -3583,8 +3583,8 @@ protected void addAliasedDeclAnnotation( pair = IPair.of(annotationToUse, new HashSet<>()); declAliases.put(annotationClass, pair); } - Set> aliases = pair.second; - aliases.add(alias); + Set> aliasSet = pair.second; + aliasSet.add(alias); } /** @@ -4010,15 +4010,16 @@ public boolean shouldWarnIfStubRedundantWithBytecode() { return null; } // Look through aliases. - IPair>> aliases = declAliases.get(annoClass); - if (aliases == null) { + IPair>> aliasPair = + declAliases.get(annoClass); + if (aliasPair == null) { return null; } - for (Class alias : aliases.second) { + for (Class alias : aliasPair.second) { for (AnnotationMirror am : declAnnos) { if (areSameByClass(am, alias)) { // TODO: need to copy over elements/fields - return aliases.first; + return aliasPair.first; } } } From ade3123bc16c098a4e74532b00cbfbb952d78255 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 28 Apr 2026 04:45:07 -0700 Subject: [PATCH 121/308] Fix formatting --- buildSrc/src/main/groovy/cf-spotless.gradle | 1 + checker/tests/modifiability/IteratorConflictingAnnosUse.java | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 6b750e1cf9fb..15481e13b81c 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -108,6 +108,7 @@ spotless { .addTypeAnnotation("UnknownReplace") .addTypeAnnotation("UnknownShrink") .addTypeAnnotation("Ungrowable") + .addTypeAnnotation("Unmodifiable") .addTypeAnnotation("Unreplaceable") .addTypeAnnotation("Unshrinkable") .addTypeAnnotation("Unmodifiable") diff --git a/checker/tests/modifiability/IteratorConflictingAnnosUse.java b/checker/tests/modifiability/IteratorConflictingAnnosUse.java index 3e5f5c07daf5..f28a0225f100 100644 --- a/checker/tests/modifiability/IteratorConflictingAnnosUse.java +++ b/checker/tests/modifiability/IteratorConflictingAnnosUse.java @@ -8,8 +8,7 @@ public class IteratorConflictingAnnosUse { private final IteratorConflictingAnnosTypeTuple inputTypes = new IteratorConflictingAnnosTypeTuple(); - void reproduce() { - @Unmodifiable Iterator inputTypeIterator = inputTypes.iterator(); - inputTypeIterator.hasNext(); + @Unmodifiable Iterator reproduce() { + return inputTypes.iterator(); } } From 5304be3f99144683fb96d87741e2133dd153ad0f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 28 Apr 2026 12:21:00 -0700 Subject: [PATCH 122/308] Checkpoint --- ...move.java => IteratorPreservesRemove.java} | 4 +- ...va => UnknownIteratorPreservesRemove.java} | 2 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 37 +++++++++++-------- .../modifiability/IteratorPrecisionTest.java | 6 +-- .../ThrowablePrintStackTrace.java | 3 ++ docs/manual/modifiability-checker.tex | 31 +++++++--------- .../framework/stub/AnnotationFileParser.java | 3 +- .../framework/type/AnnotatedTypeFactory.java | 2 +- .../framework/type/AnnotatedTypeReplacer.java | 6 +-- .../framework/util/defaults/Default.java | 2 +- .../framework/util/defaults/DefaultSet.java | 4 +- .../framework/util/element/MethodApplier.java | 12 +++--- .../TargetedElementAnnotationApplier.java | 5 ++- 13 files changed, 61 insertions(+), 56 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{IteratorPreserveRemove.java => IteratorPreservesRemove.java} (87%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownIter.java => UnknownIteratorPreservesRemove.java} (94%) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java similarity index 87% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java index c39c25d90131..4d286c85b9f4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreserveRemove.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java @@ -16,5 +16,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownIter.class) -public @interface IteratorPreserveRemove {} +@SubtypeOf(UnknownIteratorPreservesRemove.class) +public @interface IteratorPreservesRemove {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java similarity index 94% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java index fa9f70062d27..a91dc46f09af 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIter.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java @@ -22,4 +22,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownIter {} +public @interface UnknownIteratorPreservesRemove {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 70e341bc1bba..a60b10243fbc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -12,12 +12,12 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrink; -import org.checkerframework.checker.modifiability.qual.IteratorPreserveRemove; +import org.checkerframework.checker.modifiability.qual.IteratorPreservesRemove; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownIter; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPreservesRemove; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -60,10 +60,13 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyShrink} qualifier. */ private AnnotationMirror POLY_SHRINK; - /** The {@code @}{@link UnknownIter} qualifier (top of iterator-preservation hierarchy). */ + /** + * The {@code @}{@link UnknownIteratorPreservesRemove} qualifier (top of iterator-preservation + * hierarchy). + */ private AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPreserveRemove} qualifier. */ + /** The {@code @}{@link IteratorPreservesRemove} qualifier. */ private AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -86,9 +89,10 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); - this.UNKNOWN_ITER = AnnotationBuilder.fromClass(getElementUtils(), UnknownIter.class); + this.UNKNOWN_ITER = + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPreservesRemove.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPreserveRemove.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPreservesRemove.class); addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNSHRINKABLE); @@ -106,8 +110,8 @@ protected Set> createSupportedTypeQualifiers() { Unshrinkable.class, BottomShrink.class, PolyShrink.class, - UnknownIter.class, - IteratorPreserveRemove.class)); + UnknownIteratorPreservesRemove.class, + IteratorPreservesRemove.class)); } @Override @@ -139,11 +143,12 @@ protected ParameterizedExecutableType methodFromUse( } /** - * Refines {@code iterator()} return type based on {@code @IteratorPreserveRemove}. + * Refines {@code iterator()} return type based on {@code @IteratorPreservesRemove}. * *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked - * method receiver type has {@code @IteratorPreserveRemove}, then the result is {@code @Shrinkable - * Iterator}. Otherwise, shrinkability precision is dropped to {@code @UnknownShrink}. + * method receiver type has {@code @IteratorPreservesRemove}, then the result is + * {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to + * {@code @UnknownShrink}. */ private void refineIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { @@ -171,8 +176,8 @@ private void refineIteratorReturnType( return; } - // receiver type is @Shrinkable. check for @IteratorPreserveRemove - if (hasIteratorPreserveRemove(receiverType)) { + // receiver type is @Shrinkable. check for @IteratorPreservesRemove + if (hasIteratorPreservesRemove(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { returnType.replaceAnnotation(UNKNOWN_SHRINK); @@ -194,13 +199,13 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); } - /** Returns true if {@code type} has the {@code @IteratorPreserveRemove} marker annotation. */ - private boolean hasIteratorPreserveRemove(AnnotatedTypeMirror type) { + /** Returns true if {@code type} has the {@code @IteratorPreservesRemove} marker annotation. */ + private boolean hasIteratorPreservesRemove(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPreserveRemove.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPreservesRemove.class); } /** diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 589832146b34..42a7681c2024 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -24,7 +24,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; -import org.checkerframework.checker.modifiability.qual.IteratorPreserveRemove; +import org.checkerframework.checker.modifiability.qual.IteratorPreservesRemove; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -37,7 +37,7 @@ enum TestEnum { B } - @IteratorPreserveRemove List list; + @IteratorPreservesRemove List list; void iteratorChecker() { list = @@ -67,7 +67,7 @@ void copyOnWriteArrayListIterator() { List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), - // then if the receiver is @Shrinkable and has @IteratorPreserveRemove, then the result is + // then if the receiver is @Shrinkable and has @IteratorPreservesRemove, then the result is // @Shrinkable. // otherwise, the result is unknown. // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? diff --git a/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java b/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java index 84aaebb00508..9e61b3aad450 100644 --- a/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java +++ b/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java @@ -12,6 +12,7 @@ class ThrowablePrintStackTrace implements Closeable { private @Owning CloseableResource resource = new CloseableResource(); + @SuppressWarnings("contracts.postcondition") // temporary, to remove before merging @Override @EnsuresCalledMethods(value = "this.resource", methods = "close") public void close() { @@ -25,6 +26,7 @@ class ThrowablePrintStackTracePrintStream implements Closeable { private @Owning CloseableResource resource = new CloseableResource(); + @SuppressWarnings("contracts.postcondition") // temporary, to remove before merging @Override @EnsuresCalledMethods(value = "this.resource", methods = "close") public void close() { @@ -38,6 +40,7 @@ class ThrowablePrintStackTracePrintWriter implements Closeable { private @Owning CloseableResource resource = new CloseableResource(); + @SuppressWarnings("contracts.postcondition") // temporary, to remove before merging @Override @EnsuresCalledMethods(value = "this.resource", methods = "close") public void close() { diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 67a57ff26376..bcaeed84757d 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -72,13 +72,13 @@ \centering \begin{minipage}{0.85\linewidth} \begin{verbatim} - Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy + Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIter - / \ / \ / \ | - @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | - \ / \ / \ / | - @BottomGrow @BottomShrink @BottomReplace @IteratorPreserveRemove + @UnknownGrow @UnknownShrink @UnknownReplace UnknownIteratorPreservesRemove + / \ / \ / \ | + @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | + \ / \ / \ / | + @BottomGrow @BottomShrink @BottomReplace @IteratorPreservesRemove \end{verbatim} \end{minipage} \caption{The three independent Modifiability qualifier hierarchies. @@ -117,8 +117,7 @@ No guarantees are made about shrink or replace operations. \item[\refqualclass{checker/modifiability/qual}{BottomGrow}] -The bottom qualifier in the Grow hierarchy. It is a subtype of both -\<@Growable> and \<@Ungrowable>. Programmers should rarely write it. +The bottom qualifier in the Grow hierarchy. Programmers should rarely write it. \item[\refqualclass{checker/modifiability/qual}{UnknownShrink}] The top qualifier in the Shrink hierarchy. @@ -138,8 +137,7 @@ No guarantees are made about grow or replace operations. \item[\refqualclass{checker/modifiability/qual}{BottomShrink}] -The bottom qualifier in the Shrink hierarchy. It is a subtype of both -\<@Shrinkable> and \<@Unshrinkable>. Programmers should rarely write it. +The bottom qualifier in the Shrink hierarchy. Programmers should rarely write it. \item[\refqualclass{checker/modifiability/qual}{UnknownReplace}] The top qualifier in the Replace hierarchy. @@ -159,17 +157,16 @@ No guarantees are made about grow or shrink operations. \item[\refqualclass{checker/modifiability/qual}{BottomReplace}] -The bottom qualifier in the Replace hierarchy. It is a subtype of both -\<@Replaceable> and \<@Unreplaceable>. Programmers should rarely write it. +The bottom qualifier in the Replace hierarchy. Programmers should rarely write it. -\item[\refqualclass{checker/modifiability/qual}{UnknownIter}] +\item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPreservesRemove}] The top qualifier in the iterator-remove-preservation hierarchy. The checker cannot determine whether calling \code{iterator()} on this collection preserves the ability to call \code{Iterator.remove()}. This is the default qualifier for unannotated types in the iterator-remove-preservation hierarchy. -\item[\refqualclass{checker/modifiability/qual}{IteratorPreserveRemove}] +\item[\refqualclass{checker/modifiability/qual}{IteratorPreservesRemove}] Calling \code{iterator()} on this collection preserves the receiver's ability to remove elements: if the collection is \<@Shrinkable>, then the resulting \code{Iterator} is \<@Shrinkable> and its @@ -286,13 +283,13 @@ The iterator returned by a shrinkable collection does not always support \code{Iterator.remove()}. Therefore, when \code{iterator()} is called on an \<@Shrinkable> receiver, the result is \<@Shrinkable> only if the -receiver is also \<@IteratorPreserveRemove>. Otherwise, the result's +receiver is also \<@IteratorPreservesRemove>. Otherwise, the result's Shrink qualifier is conservatively \<@UnknownShrink>. When \code{iterator()} is called on an \<@Unshrinkable> receiver, the result is \<@Unshrinkable>. \begin{Verbatim} - @IteratorPreserveRemove @Shrinkable List list = ...; + @IteratorPreservesRemove @Shrinkable List list = ...; @Shrinkable Iterator it = list.iterator(); it.remove(); // OK \end{Verbatim} @@ -440,4 +437,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace -% LocalWords: UnknownIter IteratorPreserveRemove +% LocalWords: UnknownIteratorPreservesRemove IteratorPreservesRemove diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java index 268c938cdc89..5189cdba0e83 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java @@ -729,8 +729,7 @@ public static void parseJdkFileAsStub( AnnotatedTypeFactory atypeFactory, ProcessingEnvironment processingEnv, AnnotationFileAnnotations stubAnnos) { - Map options = processingEnv.getOptions(); - boolean debugAnnotationFileParser = options.containsKey("stubDebug"); + boolean debugAnnotationFileParser = processingEnv.getOptions().containsKey("stubDebug"); if (debugAnnotationFileParser) { stubDebugStatic( processingEnv, diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 9b7ca74fb72a..4c78b3dd2439 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -3554,7 +3554,7 @@ public AnnotationMirror canonicalAnnotation(AnnotationMirror a) { * @param annoClass an annotation class * @return true if the given annotation class is an alias for some other annotation */ - public boolean isAliasedTypeAnnotation(Class annoClass) { + protected boolean isAliasedTypeAnnotation(Class annoClass) { return aliases.containsKey(annoClass.getCanonicalName()); } diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java index 6f9ef73ffc1a..142b04d89387 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java @@ -94,9 +94,9 @@ public Void visitWildcard(AnnotatedWildcardType from, AnnotatedTypeMirror to) { } /** - * For type variables and wildcards, the absence of a primary annotations has an implied meaning - * on substitution. Therefore, in these cases we remove the primary annotation and rely on the - * fact that the bounds are also merged into the type to. + * For type variables and wildcards, the absence of a primary annotation has an implied meaning on + * substitution. Therefore, in these cases we remove the primary annotation and rely on the fact + * that the bounds are also merged into the type. * * @param from a type variable or wildcard * @param to the destination annotated type mirror diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/Default.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/Default.java index 87069d811dcc..289271b03ac3 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/Default.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/Default.java @@ -9,7 +9,7 @@ /** * Represents a mapping from an Annotation to a TypeUseLocation it should be applied to during * defaulting. The Comparable ordering of this class first tests location then tests annotation - * ordering (via {@link org.checkerframework.javacutil.AnnotationUtils}). + * ordering (via {@link AnnotationUtils}). * *

It also has a handy toString method that is useful for debugging. */ diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/DefaultSet.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/DefaultSet.java index 465bc253b253..0305dab16b39 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/DefaultSet.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/DefaultSet.java @@ -4,8 +4,8 @@ import org.plumelib.util.StringsPlume; /** - * An ordered set of Defaults (see {@link org.checkerframework.framework.util.defaults.Default}). - * This class provides a little syntactic sugar and a better toString over TreeSet. + * An ordered set of Defaults (see {@link Default}). This class provides a little syntactic sugar + * and a better toString over TreeSet. */ @SuppressWarnings("serial") class DefaultSet extends TreeSet { diff --git a/framework/src/main/java/org/checkerframework/framework/util/element/MethodApplier.java b/framework/src/main/java/org/checkerframework/framework/util/element/MethodApplier.java index 3e00223e0a43..5c336e15881e 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/element/MethodApplier.java +++ b/framework/src/main/java/org/checkerframework/framework/util/element/MethodApplier.java @@ -20,8 +20,8 @@ import org.plumelib.util.StringsPlume; /** - * Adds annotations from element to the return type, formal parameter types, type parameters, and - * throws clauses of the AnnotatedExecutableType type. + * Adds annotations from an element to the return type, formal parameter types, type parameters, and + * throws clauses of an AnnotatedExecutableType type. */ public class MethodApplier extends TargetedElementAnnotationApplier { @@ -121,8 +121,8 @@ protected boolean isAccepted() { } /** - * Sets the method's element, annotates its return type, parameters, type parameters, and throws - * annotations. + * Sets the method's element, and annotates its return type, parameters, type parameters, and + * throws clauses. */ @Override public void extractAndApply() throws UnexpectedAnnotationLocationException { @@ -221,8 +221,8 @@ private void applyThrowsAnnotations(List annos) } /** - * If the return type is a use of a type variable first apply the bound annotations from the type - * variables declaration. + * If the return type is a use of a type variable, first apply the bound annotations from the type + * variable's declaration. */ private void applyTypeVarUseOnReturnType() throws UnexpectedAnnotationLocationException { new TypeVarUseApplier(methodType.getReturnType(), methodSymbol, typeFactory).extractAndApply(); diff --git a/framework/src/main/java/org/checkerframework/framework/util/element/TargetedElementAnnotationApplier.java b/framework/src/main/java/org/checkerframework/framework/util/element/TargetedElementAnnotationApplier.java index 933b0056f3b5..1e388d83fd0a 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/element/TargetedElementAnnotationApplier.java +++ b/framework/src/main/java/org/checkerframework/framework/util/element/TargetedElementAnnotationApplier.java @@ -29,9 +29,10 @@ */ abstract class TargetedElementAnnotationApplier { /** - * Three annotation types that may be encountered when calling getRawTypeAttributes. see sift(). + * Three annotation groups that may be encountered when calling getRawTypeAttributes. See {@link + * #sift}. */ - static enum TargetClass { + protected static enum TargetClass { TARGETED, VALID, INVALID From 8e648755bfe3307e7a0c7ea01e4502162801e90d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 28 Apr 2026 21:26:21 -0700 Subject: [PATCH 123/308] Brevity --- .../checker/modifiability/grow/messages.properties | 2 +- .../checker/modifiability/replace/messages.properties | 2 +- .../checker/modifiability/shrink/messages.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties index 7eda6bdc0bf3..fdc062024160 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties @@ -1 +1 @@ -usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. +usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties index 7eda6bdc0bf3..fdc062024160 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties @@ -1 +1 @@ -usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. +usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties index 7eda6bdc0bf3..fdc062024160 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties @@ -1 +1 @@ -usage.throws.uoe=The method %s is annotated with @ThrowsUOE and will throw an UnsupportedOperationException if called. +usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. From de98a210fbac6c1f8166d6ec9dbf4909cf90463d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 28 Apr 2026 21:30:47 -0700 Subject: [PATCH 124/308] Make fields final --- .../grow/GrowAnnotatedTypeFactory.java | 18 ++++++++---------- .../replace/ReplaceAnnotatedTypeFactory.java | 10 +++++----- .../shrink/ShrinkAnnotatedTypeFactory.java | 18 +++++++++--------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index f9cf4966ab5d..095fd7d6b4c8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -30,7 +30,7 @@ import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.TypesUtils; -/** The type factory for the Modifiability Checker. */ +/** The annotated type factory for the {@link GrowChecker}. */ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ @@ -45,16 +45,16 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ - private AnnotationMirror UNKNOWN_GROW; + private final AnnotationMirror UNKNOWN_GROW; /** The {@code @}{@link Growable} qualifier. */ - private AnnotationMirror GROWABLE; + private final AnnotationMirror GROWABLE; /** The {@code @}{@link Ungrowable} qualifier. */ - private AnnotationMirror UNGROWABLE; + private final AnnotationMirror UNGROWABLE; /** The {@code @}{@link PolyGrow} qualifier. */ - private AnnotationMirror POLY_GROW; + private final AnnotationMirror POLY_GROW; /** * Creates a GrowAnnotatedTypeFactory. @@ -116,7 +116,7 @@ protected ParameterizedExecutableType methodFromUse( } /** - * Removes capabilities that cannot be supported by structural constraints of the collection type: + * Removes grow capability for types that structurally cannot support it: * *

    *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to @@ -167,12 +167,10 @@ protected QualifierUpperBounds createQualifierUpperBounds() { @Override public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { if (TypesUtils.isErasedSubtype(type, mapEntryErasure, types)) { - // Elements of a map entry can never be grown, so treat them as @UnknownGrow. Even if - // they are annotation @Growable in a stubfile. - return unknownGrow; + // Map.Entry cannot be grown. } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { - // It's a standard Iterator, but NOT a ListIterator: Drop G bit + // Standard Iterator (not ListIterator) cannot be grown. return unknownGrow; } return super.getBoundQualifiers(type); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 04ca6769a7d2..b7c3a028763b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -30,7 +30,7 @@ import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.TypesUtils; -/** The type factory for the Modifiability Checker. */ +/** The annotated type factory for the {@link ReplaceChecker}. */ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Set} type. */ @@ -54,16 +54,16 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ - private AnnotationMirror UNKNOWN_REPLACE; + private final AnnotationMirror UNKNOWN_REPLACE; /** The {@code @}{@link Replaceable} qualifier. */ - private AnnotationMirror REPLACEABLE; + private final AnnotationMirror REPLACEABLE; /** The {@code @}{@link Unreplaceable} qualifier. */ - private AnnotationMirror UNREPLACEABLE; + private final AnnotationMirror UNREPLACEABLE; /** The {@code @}{@link PolyReplace} qualifier. */ - private AnnotationMirror POLY_REPLACE; + private final AnnotationMirror POLY_REPLACE; /** * Creates a ReplaceAnnotatedTypeFactory. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index a60b10243fbc..32205b901cc7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -37,7 +37,7 @@ import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; -/** The type factory for the Modifiability Checker. */ +/** The annotated type factory for the {@link ShrinkChecker}. */ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ @@ -49,25 +49,25 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ - private AnnotationMirror UNKNOWN_SHRINK; + private final AnnotationMirror UNKNOWN_SHRINK; /** The {@code @}{@link Shrinkable} qualifier. */ - private AnnotationMirror SHRINKABLE; + private final AnnotationMirror SHRINKABLE; /** The {@code @}{@link Unshrinkable} qualifier. */ - private AnnotationMirror UNSHRINKABLE; + private final AnnotationMirror UNSHRINKABLE; /** The {@code @}{@link PolyShrink} qualifier. */ - private AnnotationMirror POLY_SHRINK; + private final AnnotationMirror POLY_SHRINK; /** * The {@code @}{@link UnknownIteratorPreservesRemove} qualifier (top of iterator-preservation * hierarchy). */ - private AnnotationMirror UNKNOWN_ITER; + private final AnnotationMirror UNKNOWN_ITER; /** The {@code @}{@link IteratorPreservesRemove} qualifier. */ - private AnnotationMirror ITERATOR_PRESERVE_REMOVE; + private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** * Creates a ShrinkAnnotatedTypeFactory. @@ -209,10 +209,10 @@ private boolean hasIteratorPreservesRemove(AnnotatedTypeMirror type) { } /** - * Removes capabilities that cannot be supported by structural constraints of the collection type: + * Removes shrink capability for types that structurally cannot support it: * *
      - *
    • Map.Entry: remove Shrink capabilities + *
    • Map.Entry: cannot shrink, set to {@code @UnknownShrink} *
    */ private class ShrinkTypeAnnotator extends TypeAnnotator { From 1bcff268a0e3bfa247836349d5b4b39df8186d9a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 28 Apr 2026 21:36:52 -0700 Subject: [PATCH 125/308] Report "usage.throws.uoe" only once --- .../modifiability/ModifiabilityVisitor.java | 44 ++++++++++++------- .../modifiability/replace/ReplaceVisitor.java | 6 +++ .../modifiability/shrink/ShrinkVisitor.java | 6 +++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index daafc9c24ef5..8dd99931477e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -33,25 +33,39 @@ public ModifiabilityVisitor(BaseTypeChecker checker) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - ExecutableElement method = TreeUtils.elementFromUse(node); - // Check if the method being invoked is annotated with @ThrowsUOE. - // Methods with this annotation (like SortedSet.addFirst) are guaranteed to throw - // UnsupportedOperationException at run time, so we report an error immediately. - if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { - checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); + // Methods annotated @ThrowsUOE always throw UnsupportedOperationException when called. + // When running as part of the aggregate ModifiabilityChecker, only the GrowChecker + // reports this error (shouldCheckThrowsUOE() returns false in the other two) to avoid + // tripling the diagnostic. When running a sub-checker standalone, each reports it. + if (shouldCheckThrowsUOE()) { + ExecutableElement method = TreeUtils.elementFromUse(node); + if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { + checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); + } } return super.visitMethodInvocation(node, p); } - // MDE: Ensure that there are tests that no unsoundness results. (I didn't check whether they - // exist.) - // Suppress the framework's "constructor result must be TOP" check. - // For Modifiability, constructors may legitimately produce @Modifiable. - // By default, the BaseTypeChecker requires constructors to return the top type - // (here @UnknownModifiability). However, many collection constructors (like new ArrayList()) - // produce a @Modifiable object, which is a subtype of @UnknownModifiability. - // We want to allow this so that we can use these objects for mutation without - // explicit casting or annotations. + /** + * Returns true if this checker should report {@code @ThrowsUOE} errors. + * + *

    The default is {@code true}. {@link + * org.checkerframework.checker.modifiability.shrink.ShrinkVisitor} and {@link + * org.checkerframework.checker.modifiability.replace.ReplaceVisitor} override this to return + * {@code false} when running under the aggregate {@link ModifiabilityChecker}, so that each + * {@code @ThrowsUOE} call site produces exactly one error rather than three. + * + * @return true if this visitor should report {@code @ThrowsUOE} violations + */ + protected boolean shouldCheckThrowsUOE() { + return true; + } + + // Suppresses the framework's "constructor result must be TOP" check. + // Collection constructors (e.g., new ArrayList()) legitimately produce @Modifiable, which is a + // subtype of the top type @UnknownModifiability. This suppression is sound: constructors are + // typed by their stubs or by defaults inferred from the class's annotations, so they cannot + // silently widen an unmodifiable collection to @Modifiable. @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 03dc7ea43e95..f13c02c4a69c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -14,4 +14,10 @@ public class ReplaceVisitor extends ModifiabilityVisitor { public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } + + @Override + protected boolean shouldCheckThrowsUOE() { + // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index 5d58b5382f85..83000bc897ad 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -14,4 +14,10 @@ public class ShrinkVisitor extends ModifiabilityVisitor { public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } + + @Override + protected boolean shouldCheckThrowsUOE() { + // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. + return checker.getParentChecker() == null; + } } From 1a7f466f260363ad77328572f6e29ba329b57bf8 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 29 Apr 2026 11:36:24 -0700 Subject: [PATCH 126/308] Use depth 0 rather than 1000 --- .azure/azure-pipelines-daily.yml | 10 +++++----- .azure/defs-common.m4 | 2 +- .azure/defs.m4 | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.azure/azure-pipelines-daily.yml b/.azure/azure-pipelines-daily.yml index 16b6c34a65a1..a0a1a8951919 100644 --- a/.azure/azure-pipelines-daily.yml +++ b/.azure/azure-pipelines-daily.yml @@ -228,7 +228,7 @@ jobs: container: mdernst/cf-ubuntu-jdk17:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=17 && ./checker/bin-devel/test-typecheck.sh displayName: test-typecheck.sh - job: typecheck_jdk21 @@ -241,7 +241,7 @@ jobs: container: mdernst/cf-ubuntu-jdk21:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=21 && ./checker/bin-devel/test-typecheck.sh displayName: test-typecheck.sh - job: typecheck_part1_jdk25 @@ -250,7 +250,7 @@ jobs: container: mdernst/cf-ubuntu-jdk25:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=25 && ./checker/bin-devel/test-typecheck-part1.sh displayName: test-typecheck-part1.sh - job: typecheck_part2_jdk25 @@ -259,7 +259,7 @@ jobs: container: mdernst/cf-ubuntu-jdk25:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=25 && ./checker/bin-devel/test-typecheck-part2.sh displayName: test-typecheck-part2.sh - job: typecheck_jdk26 @@ -272,7 +272,7 @@ jobs: container: mdernst/cf-ubuntu-jdk26:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=26 && ./checker/bin-devel/test-typecheck.sh displayName: test-typecheck.sh diff --git a/.azure/defs-common.m4 b/.azure/defs-common.m4 index fe5981277307..38950046e067 100644 --- a/.azure/defs-common.m4 +++ b/.azure/defs-common.m4 @@ -1,6 +1,6 @@ changequote changequote(`[',`]')dnl -ifelse([The built-in "dnl" m4 macro means "discard to next line",])dnl +ifelse([The built-in "dnl" m4 macro means "discard to next line."])dnl define([canary_os], [ubuntu])dnl define([canary_version], [25])dnl define([latest_version], [26])dnl diff --git a/.azure/defs.m4 b/.azure/defs.m4 index 864e06a65fe4..e110f93a8f20 100644 --- a/.azure/defs.m4 +++ b/.azure/defs.m4 @@ -129,7 +129,7 @@ ifelse($1,canary_version,[dnl container: mdernst/cf-ubuntu-jdk$1[]docker_testing:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=$1 && ./checker/bin-devel/test-typecheck-part1.sh displayName: test-typecheck-part1.sh - job: typecheck_part2_jdk$1 @@ -138,7 +138,7 @@ ifelse($1,canary_version,[dnl container: mdernst/cf-ubuntu-jdk$1[]docker_testing:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=$1 && ./checker/bin-devel/test-typecheck-part2.sh displayName: test-typecheck-part2.sh], [dnl - job: typecheck_jdk$1 @@ -151,7 +151,7 @@ ifelse($1,canary_version,[dnl container: mdernst/cf-ubuntu-jdk$1[]docker_testing:latest steps: - checkout: self - fetchDepth: 1000 + fetchDepth: 0 - bash: export ORG_GRADLE_PROJECT_jdkTestVersion=$1 && ./checker/bin-devel/test-typecheck.sh displayName: test-typecheck.sh])])dnl dnl From fe26f6b1e584dc89a896449861e6b557877aff69 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 29 Apr 2026 11:37:27 -0700 Subject: [PATCH 127/308] Make canary jobs consistent with Azure --- .circleci/config.yml | 19 +++++++------------ .circleci/config.yml.m4 | 8 ++++++-- .circleci/defs.m4 | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 552439c6468a..9d474fc17a20 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -391,11 +391,15 @@ workflows: - junit_part1_jdk25 - junit_part2_jdk25 - nonjunit_jdk25 + - inference_part1_jdk25 + - inference_part2_jdk25 - typecheck_part1_jdk25 - typecheck_part2_jdk25 - misc_jdk25 - misc_jdk26 + - junit_part1_jdk25 + - junit_part2_jdk25 - nonjunit_jdk25 - misc_jdk17: requires: @@ -408,10 +412,9 @@ workflows: - misc_jdk25 - misc_jdk25 - - misc_jdk26: - requires: - - canary_jobs - + - misc_jdk26 + - inference_part1_jdk25 + - inference_part2_jdk25 - typecheck_part1_jdk25 - typecheck_part2_jdk25 @@ -419,14 +422,6 @@ workflows: requires: - canary_jobs - - inference_part1_jdk25: - requires: - - canary_jobs - - - inference_part2_jdk25: - requires: - - canary_jobs - # TEMPORARILY commented until Daikon release 5.8.24. # job_dependences_not_in_canary(canary_version, daikon_part1) # job_dependences_not_in_canary(canary_version, daikon_part2) diff --git a/.circleci/config.yml.m4 b/.circleci/config.yml.m4 index 3428e8f89a9d..be72d7b6bec0 100644 --- a/.circleci/config.yml.m4 +++ b/.circleci/config.yml.m4 @@ -29,23 +29,27 @@ workflows: - junit_part1_jdk[]canary_version - junit_part2_jdk[]canary_version - nonjunit_jdk[]canary_version + - inference_part1_jdk[]canary_version + - inference_part2_jdk[]canary_version - typecheck_part1_jdk[]canary_version - typecheck_part2_jdk[]canary_version - misc_jdk[]canary_version - misc_jdk[]latest_version +job_dependences(canary_version, junit_part1) +job_dependences(canary_version, junit_part2) job_dependences(canary_version, nonjunit) job_dependences(17, misc) job_dependences(21, misc) job_dependences(canary_version, misc) job_dependences(latest_version, misc) +job_dependences(canary_version, inference_part1) +job_dependences(canary_version, inference_part2) job_dependences(canary_version, typecheck_part1) job_dependences(canary_version, typecheck_part2) ifelse([The following jobs are not canary jobs, so they run after canary jobs succeed.])dnl job_dependences_not_in_canary(latest_version, junit) -job_dependences_not_in_canary(canary_version, inference_part1) -job_dependences_not_in_canary(canary_version, inference_part2) # TEMPORARILY commented until Daikon release 5.8.24. # job_dependences_not_in_canary(canary_version, daikon_part1) # job_dependences_not_in_canary(canary_version, daikon_part2) diff --git a/.circleci/defs.m4 b/.circleci/defs.m4 index eaa4caece1a5..6c1eac8deffb 100644 --- a/.circleci/defs.m4 +++ b/.circleci/defs.m4 @@ -149,11 +149,11 @@ dnl define([job_dependences], [dnl ifelse([This is tricky because whether the ":" should appear depends on whether the subsequent "requires: exists,])dnl - $2[]_jdk$1[]dnl +ifelse($2$1,misc[]latest_version,,[dnl ifelse($1,canary_version,,[: requires: - canary_jobs ifelse($1,canary_version,,[dnl -ifelse($2$1,misc[]latest_version,,[dnl - $2_jdk[]canary_version ])dnl ])dnl From f26e6321fa7acd8cb25cdf8327f7e2594aa2bd8a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 29 Apr 2026 11:59:03 -0700 Subject: [PATCH 128/308] Add dependencies --- .circleci/config.yml | 26 ++++++++++++++++++++------ .circleci/config.yml.m4 | 10 +++++++--- .circleci/defs.m4 | 5 ++++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d474fc17a20..564b8af8aa67 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -401,6 +401,13 @@ workflows: - junit_part1_jdk25 - junit_part2_jdk25 - nonjunit_jdk25 + - misc_jdk25 + - misc_jdk26 + - inference_part1_jdk25 + - inference_part2_jdk25 + - typecheck_part1_jdk25 + - typecheck_part2_jdk25 + - misc_jdk17: requires: - canary_jobs @@ -411,16 +418,23 @@ workflows: - canary_jobs - misc_jdk25 - - misc_jdk25 - - misc_jdk26 - - inference_part1_jdk25 - - inference_part2_jdk25 - - typecheck_part1_jdk25 - - typecheck_part2_jdk25 + - junit_jdk17: + requires: + - canary_jobs + - junit_part1_jdk25 + - junit_part2_jdk25 + + - junit_jdk21: + requires: + - canary_jobs + - junit_part1_jdk25 + - junit_part2_jdk25 - junit_jdk26: requires: - canary_jobs + - junit_part1_jdk25 + - junit_part2_jdk25 # TEMPORARILY commented until Daikon release 5.8.24. # job_dependences_not_in_canary(canary_version, daikon_part1) diff --git a/.circleci/config.yml.m4 b/.circleci/config.yml.m4 index be72d7b6bec0..72ec903afee0 100644 --- a/.circleci/config.yml.m4 +++ b/.circleci/config.yml.m4 @@ -39,8 +39,6 @@ workflows: job_dependences(canary_version, junit_part1) job_dependences(canary_version, junit_part2) job_dependences(canary_version, nonjunit) -job_dependences(17, misc) -job_dependences(21, misc) job_dependences(canary_version, misc) job_dependences(latest_version, misc) job_dependences(canary_version, inference_part1) @@ -49,7 +47,13 @@ job_dependences(canary_version, typecheck_part1) job_dependences(canary_version, typecheck_part2) ifelse([The following jobs are not canary jobs, so they run after canary jobs succeed.])dnl -job_dependences_not_in_canary(latest_version, junit) +job_dependences(17, misc) +job_dependences(21, misc) +job_dependences(17, junit) +job_dependences(21, junit) +job_dependences(latest_version, junit) + +ifelse([The following jobs have no corresponding job in the canary jobs.])dnl # TEMPORARILY commented until Daikon release 5.8.24. # job_dependences_not_in_canary(canary_version, daikon_part1) # job_dependences_not_in_canary(canary_version, daikon_part2) diff --git a/.circleci/defs.m4 b/.circleci/defs.m4 index 6c1eac8deffb..b9e48c514811 100644 --- a/.circleci/defs.m4 +++ b/.circleci/defs.m4 @@ -153,7 +153,10 @@ ifelse($2$1,misc[]latest_version,,[dnl ifelse($1,canary_version,,[: requires: - canary_jobs -ifelse($1,canary_version,,[dnl +ifelse($2,junit,[dnl + - $2_part1_jdk[]canary_version + - $2_part2_jdk[]canary_version +],[dnl - $2_jdk[]canary_version ])dnl ])dnl From a507ba1527916b6faa134b04be497b3a8f54985e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 29 Apr 2026 12:56:26 -0700 Subject: [PATCH 129/308] left error comments in tests that currently do not pass (SetRefinementTest and IteratorConflictingAnnosUse) --- checker/tests/modifiability/IteratorConflictingAnnosUse.java | 1 + checker/tests/modifiability/IteratorPrecisionTest.java | 1 + checker/tests/modifiability/SetRefinementTest.java | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/checker/tests/modifiability/IteratorConflictingAnnosUse.java b/checker/tests/modifiability/IteratorConflictingAnnosUse.java index f28a0225f100..e7ba6370de89 100644 --- a/checker/tests/modifiability/IteratorConflictingAnnosUse.java +++ b/checker/tests/modifiability/IteratorConflictingAnnosUse.java @@ -9,6 +9,7 @@ public class IteratorConflictingAnnosUse { new IteratorConflictingAnnosTypeTuple(); @Unmodifiable Iterator reproduce() { + // :: error: [conflicting.annos] return inputTypes.iterator(); } } diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 42a7681c2024..913a477c5fe2 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -40,6 +40,7 @@ enum TestEnum { @IteratorPreservesRemove List list; void iteratorChecker() { + // :: error: [assignment] list = new CopyOnWriteArrayList<>(); // this should not be allowed because CopyOnWriteArrayList's // iterator does not preserve remove diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 00044706de2b..5177ee8d6381 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -25,18 +25,22 @@ public SetRefinementTest(List other) { public SetRefinementTest(List other, int dummy) { this.items.set(0, other.get(0)); + // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, boolean dummy) { this.items.set(0, other.get(0)); + // :: error: [method.invocation] this.items.set(0, other.get(0)); + // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, float dummy) { this.items.get(0); this.items.set(0, other.get(0)); + // :: error: [method.invocation] this.items.addAll(other); } @@ -82,6 +86,7 @@ public void arrayAccessReceiver(List other) { List[] arr = new ArrayList[1]; arr[0] = new ArrayList<>(); arr[0].set(0, other.get(0)); + // :: error: [method.invocation] arr[0].addAll(other); } From 91d4d63d9a3cb86dd3fbc05944df6f7f10ed6c98 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 29 Apr 2026 14:33:38 -0700 Subject: [PATCH 130/308] fix spotlessApply Error --- checker/tests/modifiability/IteratorPrecisionTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 913a477c5fe2..99d7f16cd148 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -40,10 +40,9 @@ enum TestEnum { @IteratorPreservesRemove List list; void iteratorChecker() { + // this should not be allowed because CopyOnWriteArrayList's iterator does not preserve remove // :: error: [assignment] - list = - new CopyOnWriteArrayList<>(); // this should not be allowed because CopyOnWriteArrayList's - // iterator does not preserve remove + list = new CopyOnWriteArrayList<>(); @Shrinkable Iterator iterator = list.iterator(); } From e804447f4fcbcdb8b463fd545f3de718baf1cd55 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 29 Apr 2026 15:47:28 -0700 Subject: [PATCH 131/308] update manual on special case @Modifiable Iterator, separate qualifier hierarchy into 4 lists. --- docs/manual/modifiability-checker.tex | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index bcaeed84757d..947d819d0f03 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -77,7 +77,7 @@ @UnknownGrow @UnknownShrink @UnknownReplace UnknownIteratorPreservesRemove / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | - \ / \ / \ / | + \ / \ / \ / | @BottomGrow @BottomShrink @BottomReplace @IteratorPreservesRemove \end{verbatim} \end{minipage} @@ -98,8 +98,8 @@ \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} +Qualifiers in the Grow hierarchy: \begin{description} - \item[\refqualclass{checker/modifiability/qual}{UnknownGrow}] The top qualifier in the Grow hierarchy. The checker cannot determine whether this collection supports grow @@ -118,7 +118,10 @@ \item[\refqualclass{checker/modifiability/qual}{BottomGrow}] The bottom qualifier in the Grow hierarchy. Programmers should rarely write it. +\end{description} +Qualifiers in the Shrink hierarchy: +\begin{description} \item[\refqualclass{checker/modifiability/qual}{UnknownShrink}] The top qualifier in the Shrink hierarchy. The checker cannot determine whether this collection supports shrink @@ -138,7 +141,10 @@ \item[\refqualclass{checker/modifiability/qual}{BottomShrink}] The bottom qualifier in the Shrink hierarchy. Programmers should rarely write it. +\end{description} +Qualifiers in the Replace hierarchy: +\begin{description} \item[\refqualclass{checker/modifiability/qual}{UnknownReplace}] The top qualifier in the Replace hierarchy. The checker cannot determine whether this collection supports replace @@ -158,7 +164,10 @@ \item[\refqualclass{checker/modifiability/qual}{BottomReplace}] The bottom qualifier in the Replace hierarchy. Programmers should rarely write it. +\end{description} +Qualifiers in the Iterator hierarchy: +\begin{description} \item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPreservesRemove}] The top qualifier in the iterator-remove-preservation hierarchy. The checker cannot determine whether calling \code{iterator()} on this @@ -274,8 +283,10 @@ \<@UnknownGrow> and \<@UnknownReplace>: \begin{itemize} - \item \code{@Growable @Shrinkable @Replaceable Iterator} $\Rightarrow$ + \item \code{@Modifiable Iterator} $\Rightarrow$ treated as \code{@UnknownGrow @Shrinkable @UnknownReplace Iterator} + \item \code{@Unmodifiable Iterator} $\Rightarrow$ + treated as \code{@UnknownGrow @Unshrinkable @UnknownReplace Iterator} \item \code{@Growable Iterator} $\Rightarrow$ treated as \code{@UnknownGrow @UnknownShrink @UnknownReplace Iterator} \end{itemize} From f207961721b68b1f3346228dc26c256d22e549b2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 29 Apr 2026 22:38:27 -0700 Subject: [PATCH 132/308] update modifiability manual Co-authored-by: Copilot --- docs/manual/modifiability-checker.tex | 149 ++++++++++++++------------ 1 file changed, 78 insertions(+), 71 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 947d819d0f03..81a78cafbd83 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -74,7 +74,7 @@ \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace UnknownIteratorPreservesRemove + @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIteratorPreservesRemove / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | @@ -237,75 +237,6 @@ @UnknownModifiability List unknown = ...; // capability is unknown \end{Verbatim} - -\subsectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} - -Some Java types lack methods for certain kinds of mutation. The -Modifiability Checker automatically removes capabilities that the type -cannot support. - -\paragraphAndLabel{Set and Queue (except LinkedList).}{modifiability-set-queue} -The \code{Set} interface has no replace methods (\code{set} is a -\code{List}-only operation). Similarly, \code{Queue} has no replace -methods in general. The Modifiability Checker therefore sets the -Replace hierarchy qualifier to \<@UnknownReplace> for any \code{Set} or -non-\code{LinkedList} \code{Queue}, regardless of what is written on the -type. For example: - -\begin{itemize} - \item \code{@Growable @Shrinkable @Replaceable Set} $\Rightarrow$ - treated as \code{@Growable @Shrinkable @UnknownReplace Set} - \item \code{@Growable @Replaceable Set} $\Rightarrow$ - treated as \code{@Growable @UnknownShrink @UnknownReplace Set} - \item \code{@Growable @Shrinkable Set} $\Rightarrow$ unchanged - (\<@UnknownReplace> is already the default) -\end{itemize} - -\paragraphAndLabel{Map.Entry.}{modifiability-map-entry} -\code{Map.Entry} has only the optional \code{setValue} method (a -replace operation) and no grow or shrink operations. The Modifiability -Checker therefore sets the Grow and Shrink hierarchy qualifiers to -\<@UnknownGrow> and \<@UnknownShrink> respectively. For example: - -\begin{itemize} - \item \code{Map.@Growable @Shrinkable @Replaceable Entry} $\Rightarrow$ - treated as \code{Map.@UnknownGrow @UnknownShrink @Replaceable Entry} - \item \code{Map.@Growable Entry} $\Rightarrow$ - treated as \code{Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry} -\end{itemize} - -\paragraphAndLabel{Iterator.}{modifiability-iterator} -\code{Iterator} has only the optional \code{remove} method (a shrink -operation) and no grow or replace operations. (Note that -\code{ListIterator} is treated normally because it also has optional -\code{add} and \code{set} operations.) The Modifiability Checker -therefore sets the Grow and Replace hierarchy qualifiers to -\<@UnknownGrow> and \<@UnknownReplace>: - -\begin{itemize} - \item \code{@Modifiable Iterator} $\Rightarrow$ - treated as \code{@UnknownGrow @Shrinkable @UnknownReplace Iterator} - \item \code{@Unmodifiable Iterator} $\Rightarrow$ - treated as \code{@UnknownGrow @Unshrinkable @UnknownReplace Iterator} - \item \code{@Growable Iterator} $\Rightarrow$ - treated as \code{@UnknownGrow @UnknownShrink @UnknownReplace Iterator} -\end{itemize} - -The iterator returned by a shrinkable collection does not always support -\code{Iterator.remove()}. Therefore, when \code{iterator()} is called on -an \<@Shrinkable> receiver, the result is \<@Shrinkable> only if the -receiver is also \<@IteratorPreservesRemove>. Otherwise, the result's -Shrink qualifier is conservatively \<@UnknownShrink>. When -\code{iterator()} is called on an \<@Unshrinkable> receiver, the result -is \<@Unshrinkable>. - -\begin{Verbatim} - @IteratorPreservesRemove @Shrinkable List list = ...; - @Shrinkable Iterator it = list.iterator(); - it.remove(); // OK -\end{Verbatim} - - \subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} A polymorphic qualifier specifies that a method's return type has the @@ -359,7 +290,6 @@ \code{keySet} returns \code{@Ungrowable @Unshrinkable @UnknownReplace Set}. - \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} The Modifiability Checker supports one annotation that specifies method @@ -377,6 +307,83 @@ \end{description} +\sectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} + +Some Java types lack methods for certain kinds of mutation. The +Modifiability Checker automatically removes capabilities that the type +cannot support. + +\paragraphAndLabel{Set and Queue (except LinkedList).}{modifiability-set-queue} +The \code{Set} interface has no replace methods (\code{set} is a +\code{List}-only operation). Similarly, \code{Queue} has no replace +methods in general. The Modifiability Checker therefore sets the +Replace hierarchy qualifier to \<@UnknownReplace> for any \code{Set} or +non-\code{LinkedList} \code{Queue}, regardless of what is written on the +type. For example: + +\begin{itemize} + \item \code{@Modifiable Set} $\Rightarrow$ + treated as \code{@Growable @Shrinkable @UnknownReplace Set} + \item \code{@Unmodifiable Set} $\Rightarrow$ + treated as \code{@Ungrowable @Unshrinkable @UnknownReplace Set} + \item \code{@Growable @Replaceable Set} $\Rightarrow$ + treated as \code{@Growable @UnknownShrink @UnknownReplace Set} + \item \code{@Growable @Shrinkable Set} $\Rightarrow$ unchanged + (\<@UnknownReplace> is already the default) +\end{itemize} + +\paragraphAndLabel{Map.Entry.}{modifiability-map-entry} +\code{Map.Entry} has only the optional \code{setValue} method (a +replace operation) and no grow or shrink operations. The Modifiability +Checker therefore sets the Grow and Shrink hierarchy qualifiers to +\<@UnknownGrow> and \<@UnknownShrink> respectively. For example: + +\begin{itemize} + \item \code{Map.@Modifiable Entry} $\Rightarrow$ + treated as \code{Map.@UnknownGrow @UnknownShrink @Replaceable Entry} + \item \code{Map.@Unmodifiable Entry} $\Rightarrow$ + treated as \code{Map.@UnknownGrow @UnknownShrink @Unreplaceable Entry} + \item \code{Map.@Growable Entry} $\Rightarrow$ + treated as \code{Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry} +\end{itemize} + +\paragraphAndLabel{Iterator.}{modifiability-iterator} +\code{Iterator} has only the optional \code{remove} method (a shrink +operation) and no grow or replace operations. (Note that +\code{ListIterator} is treated normally because it also has optional +\code{add} and \code{set} operations.) The Modifiability Checker +therefore sets the Grow and Replace hierarchy qualifiers to +\<@UnknownGrow> and \<@UnknownReplace>: + +\begin{itemize} + \item \code{@Modifiable Iterator} $\Rightarrow$ + treated as \code{@UnknownGrow @Shrinkable @UnknownReplace Iterator} + \item \code{@Unmodifiable Iterator} $\Rightarrow$ + treated as \code{@UnknownGrow @Unshrinkable @UnknownReplace Iterator} + \item \code{@Growable Iterator} $\Rightarrow$ + treated as \code{@UnknownGrow @UnknownShrink @UnknownReplace Iterator} +\end{itemize} + +\sectionAndLabel{Iterator Behavior}{modifiability-iterator-behavior} + +The iterator returned by a shrinkable collection does not always support +\code{Iterator.remove()}. Therefore, when \code{iterator()} is called on +an \<@Shrinkable> receiver, the result is \<@Shrinkable> only if the +receiver is also \<@IteratorPreservesRemove>. Otherwise, the result's +Shrink qualifier is conservatively \<@UnknownShrink>. When +\code{iterator()} is called on an \<@Unshrinkable> receiver, the result +is \<@Unshrinkable>. + +\begin{Verbatim} + @IteratorPreservesRemove @Shrinkable List list = ...; + @Shrinkable Iterator it = list.iterator(); + it.remove(); // OK + + @Unshrinkable List list2 = ...; + @Unshrinkable Iterator it2 = list2.iterator(); + it2.remove(); // Error: it2 is @Unshrinkable +\end{Verbatim} + \sectionAndLabel{Examples}{modifiability-examples} From ba50e43ccd3e2cae240db5bdd37afae6c5588bca Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 30 Apr 2026 11:44:10 -0700 Subject: [PATCH 133/308] Explain `@Unmodifiable` on `Iterator` --- .../checker/modifiability/qual/Unmodifiable.java | 11 +++++++---- .../common/aliasing/qual/NonLeaked.java | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index f8d2c6fdc043..524aa4f7875f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -5,14 +5,17 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Iterator; /** - * Convenience alias meaning {@code @Ungrowable @Unshrinkable @Unreplaceable}. Calling any mutating - * operation on this collection (growing, shrinking, or replacing) will throw {@link + * Convenience alias unually meaning {@code @Ungrowable @Unshrinkable @Unreplaceable}. Calling any + * mutating operation on this collection (growing, shrinking, or replacing) may throw {@link * UnsupportedOperationException}. * - *

    This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @Ungrowable @Unshrinkable @Unreplaceable} on each annotated type. + *

    As an exception, when written on an {@link Iterator}, this means + * {@code @Unknowngrowable @Unshrinkable @Unknownreplaceable}. The reason is that every iterator is + * always {@code @Unknowngrowable} and {@code @Unknownreplaceable}, because {@link Iterator} has no + * growing or replacing operations. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java index ad0b07366d57..5686d79b5b3d 100644 --- a/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java +++ b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java @@ -21,7 +21,7 @@ */ // This is a type qualifier because of a Checker Framework limitation (Issue 383), but its hierarchy -// is ignored. Once the stub parser gets updated to read non-type-qualifiers annotations on stub +// is ignored. Once the stub parser gets updated to store non-type-qualifier annotations from stub // files, this annotation won't be a type qualifier anymore. @Documented From 5d4d1336595ba14589e78986c7c9e68046932aaa Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 30 Apr 2026 13:47:09 -0700 Subject: [PATCH 134/308] started on using modifiability checker on checker framework itself --- .../afu/scenelib/util/coll/VivifyingMap.java | 3 ++- .../afu/scenelib/util/coll/WrapperMap.java | 5 ++++- build.gradle | 7 +++++++ .../index/searchindex/SearchIndexAnnotatedTypeFactory.java | 5 +++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index fa28cfbbeaf3..2d4980834db5 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -2,6 +2,7 @@ import java.util.Iterator; import java.util.Map; +import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * A {@link VivifyingMap} is a map with two additional methods: @@ -50,7 +51,7 @@ public void prune() { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { // but using an iterator affords efficient deletion. - for (Iterator> ei = entrySet().iterator(); ei.hasNext(); ) { + for (@Shrinkable Iterator> ei = entrySet().iterator(); ei.hasNext(); ) { V value = ei.next().getValue(); if (value instanceof VivifyingMap vm) { vm.prune(); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 64deccd38608..12711167df1d 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,6 +3,8 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.modifiability.qual.IteratorPreservesRemove; +import org.checkerframework.checker.modifiability.qual.PolyShrink; /** * A {@link WrapperMap} is a map all of whose methods delegate by default to those of a supplied @@ -37,7 +39,8 @@ public boolean containsValue(Object value) { } @Override - public Set> entrySet() { + public @IteratorPreservesRemove @PolyShrink Set> entrySet( + @PolyShrink WrapperMap this) { return back.entrySet(); } diff --git a/build.gradle b/build.gradle index 9fcda5071ff3..f0de24b3442b 100644 --- a/build.gradle +++ b/build.gradle @@ -880,6 +880,13 @@ subprojects { "org.checkerframework.checker.resourceleak.ResourceLeakChecker") createCheckTypeTask(project.name, "Signature", "org.checkerframework.checker.signature.SignatureChecker") + createCheckTypeTask(project.name, "Modifiability", + "org.checkerframework.checker.modifiability.ModifiabilityChecker", + [ + "-Awarns", + "-Afilenames", + ]) + // The checkNullness task runs on all code, but it only *checks* the following code: // * All files outside the "framework", "checker", and "annotation-file-utilities" subprojects. diff --git a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java index 7bc44f2cc792..c2ab3d0e4b01 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java @@ -15,6 +15,7 @@ import org.checkerframework.checker.index.qual.SearchIndexBottom; import org.checkerframework.checker.index.qual.SearchIndexFor; import org.checkerframework.checker.index.qual.SearchIndexUnknown; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; @@ -89,7 +90,7 @@ protected QualifierHierarchy createQualifierHierarchy() { * @param am a @NegativeIndexFor or @SearchIndexFor annotation * @return the {@code value} field/element of the given annotation */ - private List getValueElement(AnnotationMirror am) { + private @Shrinkable List getValueElement(AnnotationMirror am) { if (areSameByClass(am, NegativeIndexFor.class)) { return AnnotationUtils.getElementValueArray(am, negativeIndexForValueElement, String.class); } else if (areSameByClass(am, SearchIndexFor.class)) { @@ -175,7 +176,7 @@ public AnnotationMirror leastUpperBoundQualifiers(AnnotationMirror a1, Annotatio // annotation that includes only their overlapping values. // Each annotation is either NegativeIndexFor or SearchIndexFor. - List arrayIntersection = getValueElement(a1); + @Shrinkable List arrayIntersection = getValueElement(a1); arrayIntersection.retainAll(getValueElement(a2)); // intersection if (arrayIntersection.isEmpty()) { From 5f9ba2978404cdda6c6a4aeaa3e83199bfb083ff Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 30 Apr 2026 14:22:13 -0700 Subject: [PATCH 135/308] add javadoc in ShrinkAnnotatedTypeFactory. --- .../shrink/ShrinkAnnotatedTypeFactory.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 32205b901cc7..7830a9010ebc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -149,6 +149,9 @@ protected ParameterizedExecutableType methodFromUse( * method receiver type has {@code @IteratorPreservesRemove}, then the result is * {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to * {@code @UnknownShrink}. + * + * @param tree the iterator method invocation + * @param methodType the annotated executable type of the invoked method */ private void refineIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { @@ -186,6 +189,10 @@ private void refineIteratorReturnType( /** * Returns true if this invocation is an instance {@code iterator()} method returning Iterator. + * + * @param tree the method invocation to test + * @param methodType the annotated executable type of the invoked method + * @return true if this invocation is an instance {@code iterator()} method returning Iterator */ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableType methodType) { if (!tree.getArguments().isEmpty()) { @@ -199,7 +206,12 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); } - /** Returns true if {@code type} has the {@code @IteratorPreservesRemove} marker annotation. */ + /** + * Returns true if {@code type} has the {@code @IteratorPreservesRemove} marker annotation. + * + * @param type the type to test + * @return true if {@code type} has the {@code @IteratorPreservesRemove} marker annotation + */ private boolean hasIteratorPreservesRemove(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; From 76dc0462b6e163f7bf3a9edab9c3807f3578a15c Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 30 Apr 2026 15:50:20 -0700 Subject: [PATCH 136/308] Remove duplicate message key definitions in Modifiability Checker --- .../checker/modifiability/ModifiabilityBaseChecker.java | 6 ++++++ .../checker/modifiability/grow/GrowChecker.java | 4 ++-- .../checker/modifiability/grow/messages.properties | 1 - .../checker/modifiability/replace/ReplaceChecker.java | 4 ++-- .../checker/modifiability/replace/messages.properties | 1 - .../checker/modifiability/shrink/ShrinkChecker.java | 4 ++-- .../checker/modifiability/shrink/messages.properties | 1 - .../checker/modifiability/messages.properties | 2 +- 8 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java new file mode 100644 index 000000000000..8b315e1d4ed5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -0,0 +1,6 @@ +package org.checkerframework.checker.modifiability; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** Base class for the Modifiability sub-checkers. */ +public abstract class ModifiabilityBaseChecker extends BaseTypeChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 4eef7697ab9d..3929944c474e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -4,7 +4,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; -import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; @@ -28,7 +28,7 @@ }) @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"growable", "modifiability"}) -public class GrowChecker extends BaseTypeChecker { +public class GrowChecker extends ModifiabilityBaseChecker { /** Creates a Grow checker. */ public GrowChecker() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties deleted file mode 100644 index fdc062024160..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/messages.properties +++ /dev/null @@ -1 +0,0 @@ -usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index 1babf9832c7a..f3367574758d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -4,7 +4,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; -import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; @@ -28,7 +28,7 @@ }) @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"replaceable", "modifiability"}) -public class ReplaceChecker extends BaseTypeChecker { +public class ReplaceChecker extends ModifiabilityBaseChecker { /** Creates a Replace checker. */ public ReplaceChecker() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties deleted file mode 100644 index fdc062024160..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/messages.properties +++ /dev/null @@ -1 +0,0 @@ -usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index 3ff962f261bd..ab946674e35a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -4,7 +4,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; -import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; @@ -28,7 +28,7 @@ }) @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"shrinkable", "modifiability"}) -public class ShrinkChecker extends BaseTypeChecker { +public class ShrinkChecker extends ModifiabilityBaseChecker { /** Creates a Shrink checker. */ public ShrinkChecker() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties deleted file mode 100644 index fdc062024160..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/messages.properties +++ /dev/null @@ -1 +0,0 @@ -usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index 95de156cf40a..fdc062024160 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1 +1 @@ -usage.throws.uoe=Method %s always throws an UnsupportedOperationException +usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. From dbf4580c8de02afffcd95906501bdc000293fd87 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 30 Apr 2026 16:22:52 -0700 Subject: [PATCH 137/308] fixed modifiabilityBaseChecker.java --- .../checker/modifiability/ModifiabilityBaseChecker.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index 8b315e1d4ed5..cd76ef32cb0f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -3,4 +3,8 @@ import org.checkerframework.common.basetype.BaseTypeChecker; /** Base class for the Modifiability sub-checkers. */ -public abstract class ModifiabilityBaseChecker extends BaseTypeChecker {} +public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { + + /** Creates a new ModifiabilityBaseChecker. */ + protected ModifiabilityBaseChecker() {} +} From 21868fd62c6c2346e54c21eaf14392461a3e5a04 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 30 Apr 2026 22:03:22 -0700 Subject: [PATCH 138/308] New overload `canonicalAnnotation(AnnotationMirror, TypeMirror)` --- .../units/UnitsAnnotationClassLoader.java | 5 +- docs/CHANGELOG.md | 12 +++ .../framework/type/AnnotatedTypeFactory.java | 21 +++++ .../framework/type/AnnotatedTypeMirror.java | 34 +++++---- .../framework/util/ContractsFromMethod.java | 76 ++++++++++++++----- 5 files changed, 115 insertions(+), 33 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java index e62adb681d48..1f071de5e1bf 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java @@ -20,9 +20,10 @@ public UnitsAnnotationClassLoader(BaseTypeChecker checker) { *

    This filter will ignore (by returning false) any units annotation which is an alias of * another base unit annotation (identified via {@link UnitsMultiple} meta-annotation). Alias * annotations can still be used in source code; they are converted into a base annotation by - * {@link UnitsAnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror)}. This filter simply + * {@link UnitsAnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror)} or {@link + * UnitsAnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror,TypeMirror)}. This filter simply * makes sure that the alias annotations themselves don't become part of the type hierarchy as - * their base annotations already are in the hierarchy. + * their canonical annotations already are in the hierarchy. */ @Override protected boolean isSupportedAnnotationClass(Class annoClass) { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9de1142707b9..610854516d9e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,18 @@ +## Version 4.1.1 + +### User-visible changes + +### Implementation details + +In `AnnotatedTypeFactory`: + +* new overload `canonicalAnnotation(AnnotationMirror, TypeMirror)` + +### Closed issues + ## Version 4.1.0 (2026-05-01) ### User-visible changes diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 4c78b3dd2439..b8560b4b5596 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -3548,6 +3548,27 @@ public AnnotationMirror canonicalAnnotation(AnnotationMirror a) { } } + /** + * Returns the canonical annotation for the passed annotation, when applied to the given type. May + * return its argument. + * + *

    This method {@code canonicalAnnotation} is called by {@link + * AnnotatedTypeMirror#addAnnotation}, so it is called for every annotation added to a type. + * + *

    This implementation handles when the passed annotation is an alias of another annotation. + * Subclasses can do additional work. + * + *

    If the canonicalization does not depend on the {@code TypeMirror}, then you may override + * {@link #canonicalAnnotation(AnnotationMirror)} instead. + * + * @param a the qualifier to canonicalize + * @param tm the type the qualifier is applied to, or null + * @return the canonical annotation, which may be the given annotation + */ + public AnnotationMirror canonicalAnnotation(AnnotationMirror a, @Nullable TypeMirror tm) { + return canonicalAnnotation(a); + } + /** * Returns true if the given annotation class is an alias for some other annotation. * diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java index 426d8843bb47..b4fdc063a63e 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java @@ -258,7 +258,7 @@ public boolean hasPrimaryAnnotationInHierarchy(AnnotationMirror annotation) { if (primaryAnnotations.isEmpty()) { return null; } - annotation = atypeFactory.canonicalAnnotation(annotation); + annotation = atypeFactory.canonicalAnnotation(annotation, underlyingType); if (!atypeFactory.isSupportedQualifier(annotation)) { // This can happen if the annotation is unrelated to this AnnotatedTypeMirror. return null; @@ -285,7 +285,7 @@ public boolean hasPrimaryAnnotationInHierarchy(AnnotationMirror annotation) { * @return an annotation from the same hierarchy as {@code annotation} if present */ public @Nullable AnnotationMirror getAnnotationInHierarchy(AnnotationMirror annotation) { - annotation = atypeFactory.canonicalAnnotation(annotation); + annotation = atypeFactory.canonicalAnnotation(annotation, underlyingType); if (atypeFactory.isSupportedQualifier(annotation)) { QualifierHierarchy qualHierarchy = this.atypeFactory.getQualifierHierarchy(); AnnotationMirror anno = @@ -613,9 +613,11 @@ public boolean hasExplicitAnnotation(Class a) { * Adds the canonical version of {@code annotation} as a primary annotation of this type and, in * the case of {@link AnnotatedTypeVariable}s, {@link AnnotatedWildcardType}s, and {@link * AnnotatedIntersectionType}s, adds it to all bounds. (The canonical version is found via {@link - * AnnotatedTypeFactory#canonicalAnnotation}.) If the canonical version of {@code annotation} is - * not a supported qualifier, then no annotation is added. If this type already has annotation in - * the same hierarchy as {@code annotation}, the behavior of this method is undefined. + * AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror)} or {@link + * AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror,TypeMirror)}.) If the canonical + * version of {@code annotation} is not a supported qualifier, then no annotation is added. If + * this type already has annotation in the same hierarchy as {@code annotation}, the behavior of + * this method is undefined. * * @param annotation the annotation to add */ @@ -623,7 +625,7 @@ public void addAnnotation(AnnotationMirror annotation) { if (annotation == null) { throw new BugInCF("AnnotatedTypeMirror.addAnnotation: null argument."); } - annotation = atypeFactory.canonicalAnnotation(annotation); + annotation = atypeFactory.canonicalAnnotation(annotation, underlyingType); if (atypeFactory.isSupportedQualifier(annotation)) { this.primaryAnnotations.add(annotation); } @@ -644,10 +646,11 @@ public void replaceAnnotation(AnnotationMirror a) { * Adds the canonical version of all {@code annotations} as primary annotations of this type and, * in the case of {@link AnnotatedTypeVariable}s, {@link AnnotatedWildcardType}s, and {@link * AnnotatedIntersectionType}s, adds them to all bounds. (The canonical version is found via - * {@link AnnotatedTypeFactory#canonicalAnnotation}.) If the canonical version of an {@code - * annotation} is not a supported qualifier, then that annotation is not add added. If this type - * already has annotation in the same hierarchy as any of the {@code annotations}, the behavior of - * this method is undefined. + * {@link AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror)} or {@link + * AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror,TypeMirror)}.) If the canonical + * version of an annotation is not a supported qualifier, then that annotation is not added. If + * this type already has annotation in the same hierarchy as any of the {@code annotations}, the + * behavior of this method is undefined. * * @param annotations the annotations to add */ @@ -664,8 +667,9 @@ public void addAnnotations(Iterable annotations) { *

    The canonical version of the {@code annotations} are added as primary annotations of this * type and, in the case of {@link AnnotatedTypeVariable}s, {@link AnnotatedWildcardType}s, and * {@link AnnotatedIntersectionType}s, adds them to all bounds. (The canonical version is found - * via {@link AnnotatedTypeFactory#canonicalAnnotation}.) If the canonical version of an - * annotation is not a supported qualifier, then that annotation is not add added. + * via {@link AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror)} or {@link + * AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror,TypeMirror)}.) If the canonical + * version of an annotation is not a supported qualifier, then that annotation is not add added. * * @param annotations the annotations to add */ @@ -682,8 +686,10 @@ public void addMissingAnnotations(Iterable annotatio *

    The canonical version of the {@code annotation} is added as a primary annotation of this * type and (in the case of {@link AnnotatedTypeVariable}s, {@link AnnotatedWildcardType}s, and * {@link AnnotatedIntersectionType}s) added to all bounds. (The canonical version is found via - * {@link AnnotatedTypeFactory#canonicalAnnotation}.) If the canonical version of an {@code - * annotation} is not a supported qualifier, then that annotation is not add added. + * {@link AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror)} or {@link + * AnnotatedTypeFactory#canonicalAnnotation(AnnotationMirror,TypeMirror)}.) If the canonical + * version of an {@code annotation} is not a supported qualifier, then that annotation is not add + * added. * * @param annotation the annotations to add */ diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 16cb11061b7f..374c5591e9f6 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -1,5 +1,6 @@ package org.checkerframework.framework.util; +import com.sun.source.tree.MethodTree; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -10,8 +11,11 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; +import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.JavaExpressionParseException; import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; import org.checkerframework.framework.qual.EnsuresQualifier; import org.checkerframework.framework.qual.EnsuresQualifierIf; @@ -153,16 +157,26 @@ private Set getContractsOfKind( // contractAnno is the meta-annotation on anno, such as PreconditionAnnotation, // PostconditionAnnotation, or ConditionalPostconditionAnnotation. AnnotationMirror contractAnno = r.second; - AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnno, anno); - if (enforcedQualifier == null) { - continue; - } List expressions = factory.getContractExpressions(kind, anno); Collections.sort(expressions); Boolean ensuresQualifierIfResult = factory.getEnsuresQualifierIfResult(kind, anno); + MethodTree methodDecl; + if (executableElement.getSimpleName().equals("")) { + // It's a constructor, not a method. + methodDecl = null; + } else { + methodDecl = (MethodTree) factory.declarationFromElement(executableElement); + } + for (String expr : expressions) { + TypeMirror exprType = getExprType(expr, methodDecl); + AnnotationMirror enforcedQualifier = + getQualifierEnforcedByContractAnnotation(contractAnno, anno, exprType); + if (enforcedQualifier == null) { + continue; + } + T contract = clazz.cast( Contract.create(kind, expr, enforcedQualifier, anno, ensuresQualifierIfResult)); @@ -172,6 +186,28 @@ private Set getContractsOfKind( return result; } + /** + * Returns the type of the given expression. + * + * @param expr an expression + * @param methodDecl the method at which to interpret the expression + * @return the type of the given expression + */ + private TypeMirror getExprType(String expr, MethodTree methodDecl) { + if (methodDecl == null) { + return null; + } else { + TypeMirror exprType; + try { + JavaExpression jExpr = + StringToJavaExpression.atMethodBody(expr, methodDecl, factory.getChecker()); + return jExpr.getType(); + } catch (JavaExpressionParseException e) { + return null; + } + } + } + /** * Returns the contracts expressed by the given framework contract annotation. * @@ -189,12 +225,6 @@ private Set getContract( return Collections.emptySet(); } - AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnnotation); - if (enforcedQualifier == null) { - return Collections.emptySet(); - } - List expressions = factory.getContractExpressions(contractAnnotation); Collections.sort(expressions); @@ -203,6 +233,13 @@ private Set getContract( Set result = new LinkedHashSet<>(); for (String expr : expressions) { + TypeMirror exprType = getExprType(expr, methodDecl); + AnnotationMirror enforcedQualifier = + getQualifierEnforcedByContractAnnotation(contractAnno, anno, exprType); + if (enforcedQualifier == null) { + continue; + } + T contract = clazz.cast( Contract.create( @@ -217,11 +254,12 @@ private Set getContract( * contractAnno}. May return null. * * @param contractAnno a pre- or post-condition annotation, such as {@code @RequiresQualifier} + * @param type the type to which the annotation applies * @return the type annotation specified in {@code contractAnno.qualifier} */ private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( - AnnotationMirror contractAnno) { - return getQualifierEnforcedByContractAnnotation(contractAnno, null, null); + AnnotationMirror contractAnno, @Nullable TypeMirror type) { + return getQualifierEnforcedByContractAnnotation(contractAnno, null, null, type); } /** @@ -230,14 +268,16 @@ private Set getContract( * * @param contractAnno a pre- or post-condition annotation, such as {@code @RequiresQualifier} * @param argumentAnno supplies the elements/fields in the return value + * @param type the type to which the annotation applies * @return the type annotation specified in {@code contractAnno.qualifier} */ private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( - AnnotationMirror contractAnno, AnnotationMirror argumentAnno) { + AnnotationMirror contractAnno, AnnotationMirror argumentAnno, @Nullable TypeMirror type) { Map argumentRenaming = makeArgumentRenaming(argumentAnno.getAnnotationType().asElement()); - return getQualifierEnforcedByContractAnnotation(contractAnno, argumentAnno, argumentRenaming); + return getQualifierEnforcedByContractAnnotation( + contractAnno, argumentAnno, argumentRenaming, type); } /** @@ -254,13 +294,15 @@ private Set getContract( * @param argumentAnno annotation containing the element {@code values}, or {@code null} * @param argumentRenaming renaming of argument names, which maps from names in {@code * argumentAnno} to names used in the returned annotation, or {@code null} + * @param type the type to which the annotation applies * @return a qualifier whose type is that of {@code contractAnno.qualifier}, or an alias for it, * or null if it is not a supported qualifier of the type system */ private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( AnnotationMirror contractAnno, @Nullable AnnotationMirror argumentAnno, - @Nullable Map argumentRenaming) { + @Nullable Map argumentRenaming, + @Nullable TypeMirror type) { @SuppressWarnings("deprecation") // permitted for use in the framework Name c = AnnotationUtils.getElementValueClassName(contractAnno, "qualifier", false); @@ -275,7 +317,7 @@ private Set getContract( anno = builder.build(); } - anno = factory.canonicalAnnotation(anno); + anno = factory.canonicalAnnotation(anno, type); if (factory.isSupportedQualifier(anno)) { return anno; } else { From 59e4133a7e7f5370a46c37e4da2904599c51beab Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 30 Apr 2026 22:23:16 -0700 Subject: [PATCH 139/308] Fixes --- .../framework/util/ContractsFromMethod.java | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 374c5591e9f6..fda9a60fa8c8 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -125,13 +125,21 @@ public Set getConditionalPostconditions( */ private Set getContractsOfKind( ExecutableElement executableElement, Contract.Kind kind, Class clazz) { + MethodTree methodDecl; + if (executableElement.getSimpleName().toString().equals("")) { + // It's a constructor, not a method. + methodDecl = null; + } else { + methodDecl = (MethodTree) factory.declarationFromElement(executableElement); + } + Set result = new LinkedHashSet<>(); // Check for a single framework-defined contract annotation. // The result is RequiresQualifier, EnsuresQualifier, EnsuresQualifierIf, or null. AnnotationMirror frameworkContractAnno = factory.getDeclAnnotation(executableElement, kind.frameworkContractClass); if (frameworkContractAnno != null) { - result.addAll(getContract(kind, frameworkContractAnno, clazz)); + result.addAll(getContract(kind, frameworkContractAnno, clazz, methodDecl)); } // Check for a framework-defined wrapper around contract annotations. @@ -143,7 +151,7 @@ private Set getContractsOfKind( List frameworkContractAnnoList = factory.getContractListValues(frameworkContractListAnno); for (AnnotationMirror a : frameworkContractAnnoList) { - result.addAll(getContract(kind, a, clazz)); + result.addAll(getContract(kind, a, clazz, methodDecl)); } } @@ -161,18 +169,10 @@ private Set getContractsOfKind( Collections.sort(expressions); Boolean ensuresQualifierIfResult = factory.getEnsuresQualifierIfResult(kind, anno); - MethodTree methodDecl; - if (executableElement.getSimpleName().equals("")) { - // It's a constructor, not a method. - methodDecl = null; - } else { - methodDecl = (MethodTree) factory.declarationFromElement(executableElement); - } - for (String expr : expressions) { TypeMirror exprType = getExprType(expr, methodDecl); AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnno, anno, exprType); + getQualifierEnforcedByContractAnnotation(contractAnno, null, exprType); if (enforcedQualifier == null) { continue; } @@ -197,7 +197,6 @@ private TypeMirror getExprType(String expr, MethodTree methodDecl) { if (methodDecl == null) { return null; } else { - TypeMirror exprType; try { JavaExpression jExpr = StringToJavaExpression.atMethodBody(expr, methodDecl, factory.getChecker()); @@ -216,11 +215,15 @@ private TypeMirror getExprType(String expr, MethodTree methodDecl) { * @param contractAnnotation a {@link RequiresQualifier}, {@link EnsuresQualifier}, or {@link * EnsuresQualifierIf} * @param clazz the class to determine the return type + * @param methodDecl the method on which the contract is written * @return the contracts expressed by the given annotation, or the empty set if the argument is * null */ private Set getContract( - Contract.Kind kind, AnnotationMirror contractAnnotation, Class clazz) { + Contract.Kind kind, + AnnotationMirror contractAnnotation, + Class clazz, + @Nullable MethodTree methodDecl) { if (contractAnnotation == null) { return Collections.emptySet(); } @@ -235,7 +238,7 @@ private Set getContract( for (String expr : expressions) { TypeMirror exprType = getExprType(expr, methodDecl); AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnno, anno, exprType); + getQualifierEnforcedByContractAnnotation(contractAnnotation, null, exprType); if (enforcedQualifier == null) { continue; } @@ -249,19 +252,6 @@ private Set getContract( return result; } - /** - * Returns the annotation mirror as specified by the {@code qualifier} element in {@code - * contractAnno}. May return null. - * - * @param contractAnno a pre- or post-condition annotation, such as {@code @RequiresQualifier} - * @param type the type to which the annotation applies - * @return the type annotation specified in {@code contractAnno.qualifier} - */ - private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( - AnnotationMirror contractAnno, @Nullable TypeMirror type) { - return getQualifierEnforcedByContractAnnotation(contractAnno, null, null, type); - } - /** * Returns the annotation mirror as specified by the {@code qualifier} element in {@code * contractAnno}, with elements/arguments taken from {@code argumentAnno}. May return null. From ccf704f7ed014926ada3abd66c254893000b87fd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 1 May 2026 07:49:05 -0700 Subject: [PATCH 140/308] Accommodate null --- .../framework/util/ContractsFromMethod.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index fda9a60fa8c8..71bb518a8eef 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -172,7 +172,7 @@ private Set getContractsOfKind( for (String expr : expressions) { TypeMirror exprType = getExprType(expr, methodDecl); AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnno, null, exprType); + getQualifierEnforcedByContractAnnotation(contractAnno, anno, exprType); if (enforcedQualifier == null) { continue; } @@ -262,10 +262,14 @@ private Set getContract( * @return the type annotation specified in {@code contractAnno.qualifier} */ private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( - AnnotationMirror contractAnno, AnnotationMirror argumentAnno, @Nullable TypeMirror type) { + AnnotationMirror contractAnno, + @Nullable AnnotationMirror argumentAnno, + @Nullable TypeMirror type) { Map argumentRenaming = - makeArgumentRenaming(argumentAnno.getAnnotationType().asElement()); + argumentAnno == null + ? Collections.emptyMap() + : makeArgumentRenaming(argumentAnno.getAnnotationType().asElement()); return getQualifierEnforcedByContractAnnotation( contractAnno, argumentAnno, argumentRenaming, type); } From 8229dcccc1bf16a7a26a9bac7d2909cc74dc2b88 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 1 May 2026 11:25:46 -0700 Subject: [PATCH 141/308] Whitespace --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a087996a2f81..d11c7d0ce322 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -35,6 +35,7 @@ In `AnnotatedTypeFactory`: * `isAliasedTypeAnnotation()` is now protected rather than public. ### Closed issues + \#6890, #7364, #7488. ## Version 4.0.0 (2026-04-07) From fd225023f13d99449f140738e1300cc004384ccc Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 1 May 2026 12:58:46 -0700 Subject: [PATCH 142/308] Tweak downloading plume-scripts --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9be7f5e10947..378a53ebad08 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ all default: style-check # Code style; defines `style-check` and `style-fix`. CODE_STYLE_EXCLUSIONS_USER := --exclude=manual.html --exclude=annotation-file-format.html -ifeq (,$(wildcard .plume-scripts)) ifeq (,$(wildcard checker/bin-devel/.plume-scripts)) dummy := $(shell ./gradlew -q getPlumeScripts) endif +ifeq (,$(wildcard .plume-scripts)) dummy := $(shell ln -s checker/bin-devel/.plume-scripts .plume-scripts) endif include .plume-scripts/code-style.mak From 57e4358d056e2f4f1085ba9ec61d849f7634a8d4 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 1 May 2026 15:33:16 -0700 Subject: [PATCH 143/308] match with master --- .../build/annotations/AlwaysInline.java | 14 ++++++++ .../build/annotations/CheckDiscard.java | 24 +++++++++++++ .../chromium/build/annotations/Contract.java | 29 ++++++++++++++++ .../build/annotations/DoNotClassMerge.java | 19 +++++++++++ .../build/annotations/DoNotInline.java | 20 +++++++++++ .../build/annotations/DoNotStripLogs.java | 14 ++++++++ .../build/annotations/EnsuresNonNull.java | 21 ++++++++++++ .../build/annotations/EnsuresNonNullIf.java | 24 +++++++++++++ .../annotations/IdentifierNameString.java | 34 +++++++++++++++++++ .../build/annotations/Initializer.java | 20 +++++++++++ .../build/annotations/MockedInTests.java | 16 +++++++++ .../build/annotations/NullMarked.java | 14 ++++++++ .../build/annotations/NullUnmarked.java | 14 ++++++++ .../chromium/build/annotations/Nullable.java | 20 +++++++++++ .../build/annotations/OptimizeAsNonNull.java | 16 +++++++++ .../build/annotations/RequiresNonNull.java | 21 ++++++++++++ .../build/annotations/ServiceImpl.java | 24 +++++++++++++ .../build/annotations/UsedByReflection.java | 22 ++++++++++++ .../build/annotations/UsedReflectively.java | 19 +++++++++++ .../api/annotations/common/CheckForNull.java | 15 ++++++++ .../api/annotations/common/NonNull.java | 15 ++++++++ .../api/annotations/common/NullAllowed.java | 15 ++++++++ .../api/annotations/common/NullUnknown.java | 15 ++++++++ 23 files changed, 445 insertions(+) create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/AlwaysInline.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/CheckDiscard.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/Contract.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/DoNotClassMerge.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/DoNotInline.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/DoNotStripLogs.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNull.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNullIf.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/IdentifierNameString.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/Initializer.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/MockedInTests.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/NullMarked.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/NullUnmarked.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/Nullable.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/OptimizeAsNonNull.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/RequiresNonNull.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/ServiceImpl.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/UsedByReflection.java create mode 100644 checker/src/testannotations/java/org/chromium/build/annotations/UsedReflectively.java create mode 100644 checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java create mode 100644 checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java create mode 100644 checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java create mode 100644 checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/AlwaysInline.java b/checker/src/testannotations/java/org/chromium/build/annotations/AlwaysInline.java new file mode 100644 index 000000000000..d36476318105 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/AlwaysInline.java @@ -0,0 +1,14 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/AlwaysInline.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Tells R8 to always inline the annotated method/constructor. */ +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface AlwaysInline {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/CheckDiscard.java b/checker/src/testannotations/java/org/chromium/build/annotations/CheckDiscard.java new file mode 100644 index 000000000000..301774ec3c2f --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/CheckDiscard.java @@ -0,0 +1,24 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/CheckDiscard.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Causes build to assert that annotated classes / methods / fields are optimized away in release + * builds (without dcheck_always_on). + */ +@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface CheckDiscard { + /** + * Describes why the element should be discarded. + * + * @return reason for discarding (crbug links are preferred unless reason is trivial). + */ + String value(); +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/Contract.java b/checker/src/testannotations/java/org/chromium/build/annotations/Contract.java new file mode 100644 index 000000000000..00300c3061f5 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/Contract.java @@ -0,0 +1,29 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/Contract.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * See: https://github.com/uber/NullAway/wiki/Supported-Annotations#contracts + * + *

    Not directly using NullAway's annotations so that Cronet does not need the extra dep. + * + *

    + * Examples:
    + * // The contract is: "If the parameter is null, the method will return false".
    + * // NullAway infers nullness from the inverse: Returning "true" means the parameter is non-null.
    + * @Contract("null -> false")
    + * // Returning false means the second parameter is non-null.
    + * @Contract("_, null -> true")
    + * 
    + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface Contract { + String value(); +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/DoNotClassMerge.java b/checker/src/testannotations/java/org/chromium/build/annotations/DoNotClassMerge.java new file mode 100644 index 000000000000..99d7395753c9 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/DoNotClassMerge.java @@ -0,0 +1,19 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/DoNotClassMerge.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated class should never be horizontally or vertically merged. + * + *

    The annotated classes are guaranteed not to be horizontally or vertically merged by Proguard. + * Other optimizations may still apply. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface DoNotClassMerge {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/DoNotInline.java b/checker/src/testannotations/java/org/chromium/build/annotations/DoNotInline.java new file mode 100644 index 000000000000..d1e4123aa9fa --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/DoNotInline.java @@ -0,0 +1,20 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/DoNotInline.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated method or class should never be inlined. + * + *

    The annotated method (or methods on the annotated class) are guaranteed not to be inlined by + * Proguard. Other optimizations may still apply. + */ +@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +@UsedReflectively +public @interface DoNotInline {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/DoNotStripLogs.java b/checker/src/testannotations/java/org/chromium/build/annotations/DoNotStripLogs.java new file mode 100644 index 000000000000..3279905ce62f --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/DoNotStripLogs.java @@ -0,0 +1,14 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/DoNotStripLogs.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** The annotated method or class will have -maximumremovedandroidloglevel 0 applied to it. */ +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface DoNotStripLogs {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNull.java b/checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNull.java new file mode 100644 index 000000000000..ad3dcec5700a --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNull.java @@ -0,0 +1,21 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/EnsuresNonNull.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * See: https://github.com/uber/NullAway/wiki/Supported-Annotations + * + *

    Not directly using NullAway's annotations so that Cronet does not need the extra dep. + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.CLASS) +public @interface EnsuresNonNull { + /** List of fields that are non-null after the method. */ + String[] value(); +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNullIf.java b/checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNullIf.java new file mode 100644 index 000000000000..53308896528b --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/EnsuresNonNullIf.java @@ -0,0 +1,24 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/EnsuresNonNullIf.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * See: https://github.com/uber/NullAway/wiki/Supported-Annotations + * + *

    Not directly using NullAway's annotations so that Cronet does not need the extra dep. + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.CLASS) +public @interface EnsuresNonNullIf { + /** List of fields that are non-null after the method. */ + String[] value(); + + /** The return value that causes the fields to be non-null. */ + boolean result() default true; +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/IdentifierNameString.java b/checker/src/testannotations/java/org/chromium/build/annotations/IdentifierNameString.java new file mode 100644 index 000000000000..70feb05bc00e --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/IdentifierNameString.java @@ -0,0 +1,34 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/IdentifierNameString.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark field that may contain Strings referring to fully qualified class names + * and methods whose arguments may be fully qualified class names. These classes may then be + * obfuscated by R8. A couple caveats when using this: + * - This only obfuscates the string, it does not actually check that the class exists. + * - If a field has this annotation, it must be non-final, otherwise javac will inline the constant + * and R8 won't obfuscate it. + * - Any field/method must be assigned/called with a String literal or a variable R8 can easily + * trace to a String literal. + * + *

    Usage example:
    + * {@code + * @IdentifierNameString + * public static final String LOGGING_TAG = "com.google.android.apps.foo.FooActivity"; + * + * // In this example, both className and message are treated as identifier name strings, but will + * // only be obfuscated if the string points to a real class. + * @IdentifierNameString + * public void doSomeLogging(String className, String message) { ... } + * } + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface IdentifierNameString {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/Initializer.java b/checker/src/testannotations/java/org/chromium/build/annotations/Initializer.java new file mode 100644 index 000000000000..2f189efee0a0 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/Initializer.java @@ -0,0 +1,20 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/Initializer.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Causes a method to treated as a constructor for the sake of null checking. + * + *

    See: https://github.com/uber/NullAway/wiki/Supported-Annotations + * + *

    Not directly using NullAway's annotations so that Cronet does not need the extra dep. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface Initializer {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/MockedInTests.java b/checker/src/testannotations/java/org/chromium/build/annotations/MockedInTests.java new file mode 100644 index 000000000000..42d4dffb9d93 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/MockedInTests.java @@ -0,0 +1,16 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/MockedInTests.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * See b/147584922. Proguard and Mockito don't play nicely together, and proguard rules make it + * impossible to keep the base class/interface for a mocked class without providing additional + * explicit information, like this annotation. This annotation should only need to be used on a + * class/interface that is extended/implemented by another class/interface that is then mocked. + */ +@Target(ElementType.TYPE) +public @interface MockedInTests {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/NullMarked.java b/checker/src/testannotations/java/org/chromium/build/annotations/NullMarked.java new file mode 100644 index 000000000000..653e143b3ff4 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/NullMarked.java @@ -0,0 +1,14 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/NullMarked.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Ensures the annotated class or method is checked by NullAway. */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.CLASS) +public @interface NullMarked {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/NullUnmarked.java b/checker/src/testannotations/java/org/chromium/build/annotations/NullUnmarked.java new file mode 100644 index 000000000000..5896b13d77aa --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/NullUnmarked.java @@ -0,0 +1,14 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/NullUnmarked.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Ensures the annotated class or method is not checked by NullAway. */ +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.CLASS) +public @interface NullUnmarked {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/Nullable.java b/checker/src/testannotations/java/org/chromium/build/annotations/Nullable.java new file mode 100644 index 000000000000..2f008a1cbcce --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/Nullable.java @@ -0,0 +1,20 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/Nullable.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Based on jspecify's @Nullable and checked by NullAway. + * + *

    Not directly using jspecify's annotations so that Cronet does not need the extra dep. + * + *

    See: https://github.com/uber/NullAway/wiki/Supported-Annotations + */ +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +public @interface Nullable {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/OptimizeAsNonNull.java b/checker/src/testannotations/java/org/chromium/build/annotations/OptimizeAsNonNull.java new file mode 100644 index 000000000000..6740e3e76c42 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/OptimizeAsNonNull.java @@ -0,0 +1,16 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/OptimizeAsNonNull.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated method or field is never null. Helps R8 optimize until b/302213229 is implemented. + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface OptimizeAsNonNull {} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/RequiresNonNull.java b/checker/src/testannotations/java/org/chromium/build/annotations/RequiresNonNull.java new file mode 100644 index 000000000000..b9ab8bfa6154 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/RequiresNonNull.java @@ -0,0 +1,21 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/RequiresNonNull.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * See: https://github.com/uber/NullAway/wiki/Supported-Annotations + * + *

    Not directly using NullAway's annotations so that Cronet does not need the extra dep. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface RequiresNonNull { + /** List of fields that must be non-null before the method. */ + String[] value(); +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/ServiceImpl.java b/checker/src/testannotations/java/org/chromium/build/annotations/ServiceImpl.java new file mode 100644 index 000000000000..c3bb99e26ed1 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/ServiceImpl.java @@ -0,0 +1,24 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/ServiceImpl.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Generate a META-INF/services file that maps the annotated class to the given service class. + * + *

    This is the same as @AutoService, but directly supported in Chrome's build system (does not + * require an annotation processor). + */ +@Target({ElementType.TYPE}) +// Must be CLASS retention in order to cause compile_java.py to not skip @ServiceImpl parsing via +// its incremental mode. +@Retention(RetentionPolicy.CLASS) +public @interface ServiceImpl { + /** The service implemented by this class. */ + Class value(); +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/UsedByReflection.java b/checker/src/testannotations/java/org/chromium/build/annotations/UsedByReflection.java new file mode 100644 index 000000000000..15dc0a2272a0 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/UsedByReflection.java @@ -0,0 +1,22 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/UsedByReflection.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Annotation used for marking methods and fields that are called by reflection. Useful for keeping + * components that would otherwise be removed by Proguard. Use the value parameter to mention a file + * that calls this method. + * + *

    Note that adding this annotation to a method is not enough to guarantee that it is kept - + * either its class must be referenced elsewhere in the program, or the class must be annotated with + * this as well. + */ +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.CONSTRUCTOR}) +@UsedReflectively +public @interface UsedByReflection { + String value(); +} diff --git a/checker/src/testannotations/java/org/chromium/build/annotations/UsedReflectively.java b/checker/src/testannotations/java/org/chromium/build/annotations/UsedReflectively.java new file mode 100644 index 000000000000..6c90b5fc3424 --- /dev/null +++ b/checker/src/testannotations/java/org/chromium/build/annotations/UsedReflectively.java @@ -0,0 +1,19 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// https://source.chromium.org/chromium/chromium/src/+/main:build/android/java/src/org/chromium/build/annotations/UsedReflectively.java + +package org.chromium.build.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Tells Error Prone that annotated types should not be considered unused. + * + *

    Package-private since other users should use com.google.errorprone.annotations.Keep, which is + * not used here in order to avoid a dependency from //build. + */ +@Target({ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.CLASS) +@interface UsedReflectively {} diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java new file mode 100644 index 000000000000..e71b4f7cf596 --- /dev/null +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java @@ -0,0 +1,15 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/CheckForNull.html + +package org.netbeans.api.annotations.common; + +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; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD}) +public @interface CheckForNull {} diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java new file mode 100644 index 000000000000..96f262dd3e84 --- /dev/null +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java @@ -0,0 +1,15 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NonNull.html + +package org.netbeans.api.annotations.common; + +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; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +public @interface NonNull {} diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java new file mode 100644 index 000000000000..453d951a505f --- /dev/null +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java @@ -0,0 +1,15 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullAllowed.html + +package org.netbeans.api.annotations.common; + +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; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +public @interface NullAllowed {} diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java new file mode 100644 index 000000000000..dd524523fe74 --- /dev/null +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java @@ -0,0 +1,15 @@ +// Upstream version (this is a clean-room reimplementation of its interface): +// http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullUnknown.html + +package org.netbeans.api.annotations.common; + +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; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +public @interface NullUnknown {} From ef1dbe5e0ade990532d2e9d26b00db8e371b060c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 2 May 2026 09:51:58 -0700 Subject: [PATCH 144/308] Add nullness annotations --- .../checkerframework/framework/util/ContractsFromMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 71bb518a8eef..483dce6a7078 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -193,7 +193,7 @@ private Set getContractsOfKind( * @param methodDecl the method at which to interpret the expression * @return the type of the given expression */ - private TypeMirror getExprType(String expr, MethodTree methodDecl) { + private @Nullable TypeMirror getExprType(String expr, @Nullable MethodTree methodDecl) { if (methodDecl == null) { return null; } else { From 0669b864ed83c3873dd32fceff42e3ef6ee3f1bb Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 2 May 2026 12:38:05 -0700 Subject: [PATCH 145/308] Code review changes --- buildSrc/src/main/groovy/cf-spotless.gradle | 1 - .../qual/IteratorPreservesRemove.java | 7 +++++-- .../checker/modifiability/qual/Modifiable.java | 11 ++++++----- .../checker/modifiability/qual/PolyModifiable.java | 14 ++++++-------- .../checker/modifiability/qual/ThrowsUOE.java | 3 +-- .../qual/UnknownIteratorPreservesRemove.java | 7 +++---- .../checker/modifiability/qual/Unmodifiable.java | 12 +++++------- .../modifiability/ModifiabilityChecker.java | 6 +++--- .../modifiability/ModifiabilityMethodUtils.java | 2 +- .../checker/modifiability/grow/GrowChecker.java | 4 ++-- 10 files changed, 32 insertions(+), 35 deletions(-) diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 4226bfcd516d..31c3a2f16cbe 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -111,7 +111,6 @@ spotless { .addTypeAnnotation("UnknownReplace") .addTypeAnnotation("UnknownShrink") .addTypeAnnotation("Unmodifiable") - .addTypeAnnotation("Unmodifiable") .addTypeAnnotation("Unreplaceable") .addTypeAnnotation("Unshrinkable") } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java index 4d286c85b9f4..d8998c121e4c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java @@ -8,8 +8,11 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * Collections in the Java Collection Framework whose {@code iterator()} result preserves the - * ability to call {@code Iterator.remove()}. + * If a collection's type is {@code @IteratorPreservesRemove}, then its {@code iterator()} method + * preserves the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has + * type {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any + * collection whose type is {@code @UnknownIteratorPreservesRemove}, its iterator is always + * {@code @Unshrinkable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 97d492135de4..537f47bfada7 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -7,12 +7,13 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @Growable @Shrinkable @Replaceable}. Calling grow, shrink, and - * replace operations such as {@code add}, {@code remove}, {@code set}, etc. on this collection will - * not result in throwing {@link UnsupportedOperationException}. + * Convenience alias usually meaning {@code @Growable @Shrinkable @Replaceable}. Calling a mutating + * operation (growing, shrinking, or replacing) on this collection will not result in throwing + * {@link UnsupportedOperationException}. * - *

    This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @Growable @Shrinkable @Replaceable} on each annotated type. + *

    As an exception, {@code @Modifiable} means {@code @Unknown*} if the type does not support a + * given category of operation; for example, {@Modifiable Iterator} means + * {@code @UnknownGrow @UnknownShrink @Replaceable Iterator}.) * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 73bc67261eb3..87345ae94317 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -4,14 +4,11 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @PolyGrow @PolyShrink @PolyReplace}. A polymorphic qualifier for + * Convenience alias meaning {@code @PolyGrow @PolyReplace @PolyShrink}. A polymorphic qualifier for * all three modifiability hierarchies. * - *

    This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @PolyGrow @PolyShrink @PolyReplace} on each annotated type. - * - *

    Use on methods that preserve or transfer modifiability — for example, {@code - * List.subList()}, {@code iterator()}, or {@code stream()}. + *

    You should write {@code @PolyModifiable} on methods that preserve or transfer modifiability, + * such as {@code List.subList()}, {@code iterator()}, and {@code stream()}. * *

    For example: * @@ -21,8 +18,9 @@ * } *

* - * If the receiver is {@code @Unmodifiable}, the return is {@code @Unmodifiable}. If the receiver is - * {@code @Modifiable}, the return is {@code @Modifiable}. + * If the receiver type at a given call site is {@code @Unmodifiable}, the return type at that call + * site {@code @Unmodifiable}. If the receiver type is {@code @Modifiable}, the return type is + * {@code @Modifiable}. Likewise for growing and shrinking type qualifiers. * * @checker_framework.manual #modifiability-checker Modifiability Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java index 3651620510c7..1275077f2023 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java @@ -7,8 +7,7 @@ import java.lang.annotation.Target; /** - * Indicates that the annotated method always throws {@link UnsupportedOperationException} when - * called. + * The annotated method always throws {@link UnsupportedOperationException} when called. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java index a91dc46f09af..186b3c75b75f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java @@ -9,12 +9,11 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the iterator-remove-preservation hierarchy. Represents unknown behavior - * about whether calling {@code iterator()} preserves the ability to call {@code Iterator.remove()}. + * For the annotated collection, The return type of {@code iterator()} is {@code @Unshrinkable}. * - *

This is the default qualifier for unannotated types in the iterator-remove-preservation - * hierarchy. + *

This is the default qualifier for unannotated types. * + * @see #IteratorPreservesRemove * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 524aa4f7875f..56f430ac40ca 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -5,17 +5,15 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Iterator; /** - * Convenience alias unually meaning {@code @Ungrowable @Unshrinkable @Unreplaceable}. Calling any - * mutating operation on this collection (growing, shrinking, or replacing) may throw {@link + * Convenience alias usually meaning {@code @Ungrowable @Unshrinkable @Unreplaceable}. Calling a + * mutating operation (growing, shrinking, or replacing) may throw {@link * UnsupportedOperationException}. * - *

As an exception, when written on an {@link Iterator}, this means - * {@code @Unknowngrowable @Unshrinkable @Unknownreplaceable}. The reason is that every iterator is - * always {@code @Unknowngrowable} and {@code @Unknownreplaceable}, because {@link Iterator} has no - * growing or replacing operations. + *

As an exception, {@code @Unmodifiable} means {@code @Unknown*} if the type does not support a + * given category of operation; for example, {@Unmodifiable Iterator} means + * {@code @UnknownGrow @UnknownShrink @Unreplaceable Iterator}.) * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 5b79cc22a121..131f502e1506 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -16,9 +16,9 @@ * modifiability capability: * *

    - *
  • {@link GrowChecker} — checks grow operations (e.g., {@code add}, {@code addAll}) - *
  • {@link ShrinkChecker} — checks shrink operations (e.g., {@code remove}, {@code clear}) - *
  • {@link ReplaceChecker} — checks replace operations (e.g., {@code set}, {@code replaceAll}) + *
  • {@link GrowChecker} checks grow operations (e.g., {@code add}, {@code addAll}) + *
  • {@link ShrinkChecker} checks shrink operations (e.g., {@code remove}, {@code clear}) + *
  • {@link ReplaceChecker} checks replace operations (e.g., {@code set}, {@code replaceAll}) *
* * @checker_framework.manual #modifiability-checker Modifiability Checker diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java index e2a352ccc69d..d9720c39fc48 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java @@ -12,7 +12,7 @@ public final class ModifiabilityMethodUtils { /** The fully-qualified class name of plume-util's collections utilities. */ private static final String COLLECTIONS_PLUME = "org.plumelib.util.CollectionsPlume"; - /** The method name for removing duplicates from a collection. */ + /** The method name in CollectionsPlume for removing duplicates from a collection. */ private static final String WITHOUT_DUPLICATES = "withoutDuplicates"; /** This is a utility class. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 3929944c474e..004f9eae9f1a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a grow method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Growable} collections can be - * safely mutated, {@code @Ungrowable} collections cannot be grown, and {@code @UnknownGrow} - * collections have unknown grow behavior. + * safely added to, {@code @Ungrowable} collections cannot, and {@code @UnknownGrow} collections + * have unknown grow behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ From 9802e3016cf0c0840bbad023bf9bab2b3dfe9690 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 2 May 2026 13:41:47 -0700 Subject: [PATCH 146/308] Rename `IteratorPreservesRemove` to `IteratorPolyShrink` --- .../afu/scenelib/util/coll/WrapperMap.java | 4 +- buildSrc/src/main/groovy/cf-spotless.gradle | 4 +- ...vesRemove.java => IteratorPolyShrink.java} | 8 +- ...ve.java => UnknownIteratorPolyShrink.java} | 4 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 35 ++- .../modifiability/IteratorPrecisionTest.java | 14 +- docs/manual/manual-style.tex | 4 +- docs/manual/modifiability-checker.tex | 224 ++++++++++-------- 8 files changed, 167 insertions(+), 130 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{IteratorPreservesRemove.java => IteratorPolyShrink.java} (71%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownIteratorPreservesRemove.java => UnknownIteratorPolyShrink.java} (90%) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 12711167df1d..126096f425c3 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,7 +3,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.modifiability.qual.IteratorPreservesRemove; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.PolyShrink; /** @@ -39,7 +39,7 @@ public boolean containsValue(Object value) { } @Override - public @IteratorPreservesRemove @PolyShrink Set> entrySet( + public @IteratorPolyShrink @PolyShrink Set> entrySet( @PolyShrink WrapperMap this) { return back.entrySet(); } diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 31c3a2f16cbe..79be8ccc759b 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -96,7 +96,7 @@ spotless { .addTypeAnnotation("BottomReplace") .addTypeAnnotation("BottomShrink") .addTypeAnnotation("Growable") - .addTypeAnnotation("IteratorPreservesRemove") + .addTypeAnnotation("IteratorPolyShrink") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrow") .addTypeAnnotation("PolyModifiable") @@ -106,7 +106,7 @@ spotless { .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") .addTypeAnnotation("UnknownGrow") - .addTypeAnnotation("UnknownIteratorPreservesRemove") + .addTypeAnnotation("UnknownIteratorPolyShrink") .addTypeAnnotation("UnknownModifiability") .addTypeAnnotation("UnknownReplace") .addTypeAnnotation("UnknownShrink") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrink.java similarity index 71% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrink.java index d8998c121e4c..164a8f8d1308 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPreservesRemove.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrink.java @@ -8,10 +8,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * If a collection's type is {@code @IteratorPreservesRemove}, then its {@code iterator()} method + * If a collection's type is {@code @IteratorPolyShrink}, then its {@code iterator()} method * preserves the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has * type {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any - * collection whose type is {@code @UnknownIteratorPreservesRemove}, its iterator is always + * collection whose type is {@code @UnknownIteratorPolyShrink}, its iterator is always * {@code @Unshrinkable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownIteratorPreservesRemove.class) -public @interface IteratorPreservesRemove {} +@SubtypeOf(UnknownIteratorPolyShrink.class) +public @interface IteratorPolyShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java similarity index 90% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java index 186b3c75b75f..b74f7b26c438 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPreservesRemove.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java @@ -13,7 +13,7 @@ * *

This is the default qualifier for unannotated types. * - * @see #IteratorPreservesRemove + * @see #IteratorPolyShrink * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownIteratorPreservesRemove {} +public @interface UnknownIteratorPolyShrink {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 7830a9010ebc..42714636ac6b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -12,12 +12,12 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrink; -import org.checkerframework.checker.modifiability.qual.IteratorPreservesRemove; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPreservesRemove; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -61,12 +61,12 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private final AnnotationMirror POLY_SHRINK; /** - * The {@code @}{@link UnknownIteratorPreservesRemove} qualifier (top of iterator-preservation + * The {@code @}{@link UnknownIteratorPolyShrink} qualifier (top of iterator-preservation * hierarchy). */ private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPreservesRemove} qualifier. */ + /** The {@code @}{@link IteratorPolyShrink} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -90,9 +90,9 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPreservesRemove.class); + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyShrink.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPreservesRemove.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrink.class); addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNSHRINKABLE); @@ -110,8 +110,8 @@ protected Set> createSupportedTypeQualifiers() { Unshrinkable.class, BottomShrink.class, PolyShrink.class, - UnknownIteratorPreservesRemove.class, - IteratorPreservesRemove.class)); + UnknownIteratorPolyShrink.class, + IteratorPolyShrink.class)); } @Override @@ -143,12 +143,11 @@ protected ParameterizedExecutableType methodFromUse( } /** - * Refines {@code iterator()} return type based on {@code @IteratorPreservesRemove}. + * Refines {@code iterator()} return type based on {@code @IteratorPolyShrink}. * *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked - * method receiver type has {@code @IteratorPreservesRemove}, then the result is - * {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to - * {@code @UnknownShrink}. + * method receiver type has {@code @IteratorPolyShrink}, then the result is {@code @Shrinkable + * Iterator}. Otherwise, shrinkability precision is dropped to {@code @UnknownShrink}. * * @param tree the iterator method invocation * @param methodType the annotated executable type of the invoked method @@ -179,8 +178,8 @@ private void refineIteratorReturnType( return; } - // receiver type is @Shrinkable. check for @IteratorPreservesRemove - if (hasIteratorPreservesRemove(receiverType)) { + // receiver type is @Shrinkable. check for @IteratorPolyShrink + if (hasIteratorPolyShrink(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { returnType.replaceAnnotation(UNKNOWN_SHRINK); @@ -207,17 +206,17 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT } /** - * Returns true if {@code type} has the {@code @IteratorPreservesRemove} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyShrink} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPreservesRemove} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyShrink} marker annotation */ - private boolean hasIteratorPreservesRemove(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyShrink(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPreservesRemove.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrink.class); } /** diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 99d7f16cd148..f02283457b67 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -24,7 +24,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; -import org.checkerframework.checker.modifiability.qual.IteratorPreservesRemove; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -37,7 +37,7 @@ enum TestEnum { B } - @IteratorPreservesRemove List list; + @IteratorPolyShrink List list; void iteratorChecker() { // this should not be allowed because CopyOnWriteArrayList's iterator does not preserve remove @@ -67,7 +67,7 @@ void copyOnWriteArrayListIterator() { List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), - // then if the receiver is @Shrinkable and has @IteratorPreservesRemove, then the result is + // then if the receiver is @Shrinkable and has @IteratorPolyShrink, then the result is // @Shrinkable. // otherwise, the result is unknown. // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? @@ -103,7 +103,7 @@ void KeySetIterator() { @Shrinkable Iterator iter2 = keys2.iterator(); } - void setIteratorPreservesRemove() { + void setIteratorPolyShrink() { Set hashSet = new HashSet<>(); @Shrinkable Iterator hashSetIterator = hashSet.iterator(); hashSetIterator.remove(); @@ -113,7 +113,7 @@ void setIteratorPreservesRemove() { treeSetIterator.remove(); } - void dequeIteratorPreservesRemove() { + void dequeIteratorPolyShrink() { Deque arrayDeque = new ArrayDeque<>(); @Shrinkable Iterator arrayDequeIterator = arrayDeque.iterator(); arrayDequeIterator.remove(); @@ -123,7 +123,7 @@ void dequeIteratorPreservesRemove() { linkedListIterator.remove(); } - void queueIteratorPreservesRemove() { + void queueIteratorPolyShrink() { Queue priorityQueue = new PriorityQueue<>(); @Shrinkable Iterator priorityQueueIterator = priorityQueue.iterator(); priorityQueueIterator.remove(); @@ -141,7 +141,7 @@ void queueIteratorPreservesRemove() { linkedTransferQueueIterator.remove(); } - void factoryIteratorPreservesRemove() { + void factoryIteratorPolyShrink() { Set enumSet = EnumSet.of(TestEnum.A, TestEnum.B); @Shrinkable Iterator enumSetIterator = enumSet.iterator(); enumSetIterator.remove(); diff --git a/docs/manual/manual-style.tex b/docs/manual/manual-style.tex index 119e65070c47..ece44e57ea77 100644 --- a/docs/manual/manual-style.tex +++ b/docs/manual/manual-style.tex @@ -171,9 +171,9 @@ \newcommand{\refenum}[4]{\href{../api/org/checkerframework/#1/#2.html\##3#4}{\<#3>}} % Reference to Oracle (previously Sun) Javadoc. -% Arg 1: .html reference, without the .../api/ prefix +% Arg 1: .html reference, without the .../api/ prefix. Quote "#" as "\#" if it appears. % Arg 2: What will appear in the formatted manual. -% Do not use \url in the body of any macro +% Do not use \url in the body of any macro. \newcommand{\sunjavadoc}[2]{\href{https://docs.oracle.com/en/java/javase/25/docs/api/#1}{\<#2>}} % Like \sunjavadoc, but for annotations and prepends "@". \newcommand{\sunjavadocanno}[2]{\href{https://docs.oracle.com/en/java/javase/25/docs/api/#1}{\<@#2>}} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 81a78cafbd83..417eb7f16ec3 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -1,19 +1,25 @@ \chapterAndLabel{Modifiability Checker}{modifiability-checker} +\newcommand{\UnsupportedOperationException}{\} + In Java, an \emph{optional} method is one that a class may or may not -support. If the subclass does not support the method, then calling the -method throws \. Thus, whenever a client +support. (Optional methods are unrelated to the \ type.) +If the class does not support the method, then calling the +method throws \UnsupportedOperationException. Thus, whenever a client calls an optional method, there is a possibility of an exception. The Modifiability Checker eliminates that possibility. If the Modifiability Checker issues no warning, then your program is -guaranteed not to throw \code{UnsupportedOperationException} due to -invoking a mutating method (such as \code{add} or \code{remove}) on an -unmodifiable collection, map, or iterator. (This chapter generally uses -just ``collection'', for brevity.) +guaranteed not to throw \UnsupportedOperationException\ due to invoking an +optional method (such as +\sunjavadoc{java.base/java/util/Collection.html\#add(E)}{add} or +\sunjavadoc{java.base/java/util/Collection.html\#remove(java.lang.Object)}{remove}) +on an unmodifiable collection. +(For brevity, this chapter generally uses +just ``collection'' to mean ``collection, map, or iterator''.) An \emph{unmodifiable collection} is one that throws -\code{UnsupportedOperationException} if a mutating method is called. +\UnsupportedOperationException\ if a mutating method is called. The JDK provides many such collections, for example, the results of: \begin{itemize} \item @@ -36,22 +42,26 @@ \end{itemize} A \emph{modifiable collection} is one on which mutating methods can be called -without throwing \code{UnsupportedOperationException}. Many other methods, such +without throwing \UnsupportedOperationException. Many other methods, such as \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be called on any collection, whether modifiable or unmodifiable. +Some collections may be modified in some ways but not in others. A collection can be modified in three different ways: \textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements - (\, \, \, \, \, \, \ldots). + (\, \, \, \, \, \ldots). \item \textbf{Shrink}: The collection can shrink by removing - existing elements (\, \, \, \, \, \, \, \ldots). + existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing elements with new values at specific locations/keys (\, \, \, \, \, \, \ldots). \end{itemize} +\noindent +Some collections can also be modified through their iterator. + To run the Modifiability Checker: \begin{Verbatim} @@ -64,31 +74,33 @@ The Modifiability Checker uses \textbf{three independent 4-element hierarchies}, one per capability. Every annotated type carries exactly one qualifier from each hierarchy. The checker also uses a fourth, -2-element hierarchy to express whether a collection's \code{iterator} - preserves the ability to call \code{Iterator.remove()}. +2-element hierarchy to express whether \ can be called on +a collection's iterator. % TODO: add a figure that follows the sturucture of other checker manuals. \begin{figure} +\begin{smaller} \centering \begin{minipage}{0.85\linewidth} \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIteratorPreservesRemove + @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIteratorPolyShrink / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | - @BottomGrow @BottomShrink @BottomReplace @IteratorPreservesRemove + @BottomGrow @BottomShrink @BottomReplace @IteratorPolyShrink \end{verbatim} \end{minipage} - \caption{The three independent Modifiability qualifier hierarchies. +\end{smaller} + \caption{The four independent Modifiability qualifier hierarchies. Each annotated type carries one qualifier from each hierarchy.} \label{fig-modifiability-lattice} \end{figure} -Figure~\ref{fig-modifiability-lattice} shows the three hierarchies. -An assignment \code{T x = y} is valid only if the type of \code{y} is a -subtype of \code{T} in \emph{every} hierarchy simultaneously. For +Figure~\ref{fig-modifiability-lattice} shows the hierarchies. +An assignment \code{T x = y} is valid only if the compile-type of \code{y} is a +subtype of (or equal to) \code{T} in \emph{every} hierarchy simultaneously. For example, \code{@Growable @UnknownShrink @UnknownReplace List} is a subtype of \code{@UnknownGrow @UnknownShrink @UnknownReplace List} (because \<@Growable> $\le$ \<@UnknownGrow>), but it is \emph{not} a @@ -96,6 +108,47 @@ (because \<@Growable> and \<@Ungrowable> are incomparable siblings in the Grow hierarchy). +\subsectionAndLabel{Alias annotations}{modifiability-aliases} + +For convenience, the Modifiability Checker provides four alias +annotations. They are not part of any hierarchy; the checker expands +each one into its constituent qualifiers when it appears in source code. + +\begin{description} + +\item[\refqualclass{checker/modifiability/qual}{Modifiable}] +Alias for \code{@Growable @Shrinkable @Replaceable} (but see XXXXX for exceptions). +Calling any mutating operation on this collection will not throw +\UnsupportedOperationException. + +\item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] +Alias for \code{@Ungrowable @Unshrinkable @Unreplaceable} (but see XXXXX for exceptions). +The collection is definitely unmodifiable: calling any mutating operation +always throws \UnsupportedOperationException. +The checker issues a warning at every invocation of a grow, +shrink, or replace operation. + +\item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] +Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. +The checker cannot determine the modifiability of the collection. +The checker conservatively issues a warning at every invocation of a grow, +shrink, or replace operation. + +\item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] +Alias for \code{@PolyGrow @PolyShrink @PolyReplace} +(see Section~\ref{modifiability-polymorphic}). + +\end{description} + +\noindent +Examples using alias annotations: + +\begin{Verbatim} + @Modifiable List mod = new ArrayList<>(); // all three capabilities + @Unmodifiable List unmod = List.of("a"); // no capabilities + @UnknownModifiability List unknown = ...; // capability is unknown +\end{Verbatim} + \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} Qualifiers in the Grow hierarchy: @@ -108,13 +161,11 @@ \item[\refqualclass{checker/modifiability/qual}{Growable}] Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this -collection will not result in throwing \code{UnsupportedOperationException}. -No guarantees are made about shrink or replace operations. +collection will not result in throwing \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Ungrowable}] Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this -collection will throw \code{UnsupportedOperationException}. -No guarantees are made about shrink or replace operations. +collection will throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{BottomGrow}] The bottom qualifier in the Grow hierarchy. Programmers should rarely write it. @@ -131,13 +182,11 @@ \item[\refqualclass{checker/modifiability/qual}{Shrinkable}] Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on this collection will not result in throwing -\code{UnsupportedOperationException}. -No guarantees are made about grow or replace operations. +\UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Unshrinkable}] Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on -this collection will throw \code{UnsupportedOperationException}. -No guarantees are made about grow or replace operations. +this collection will throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{BottomShrink}] The bottom qualifier in the Shrink hierarchy. Programmers should rarely write it. @@ -154,13 +203,11 @@ \item[\refqualclass{checker/modifiability/qual}{Replaceable}] Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ on this collection will not result in throwing -\code{UnsupportedOperationException}. -No guarantees are made about grow or shrink operations. +\UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Unreplaceable}] Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ -on this collection will throw \code{UnsupportedOperationException}. -No guarantees are made about grow or shrink operations. +on this collection will throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{BottomReplace}] The bottom qualifier in the Replace hierarchy. Programmers should rarely write it. @@ -168,18 +215,16 @@ Qualifiers in the Iterator hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPreservesRemove}] -The top qualifier in the iterator-remove-preservation hierarchy. -The checker cannot determine whether calling \code{iterator()} on this -collection preserves the ability to call \code{Iterator.remove()}. -This is the default qualifier for unannotated types in the -iterator-remove-preservation hierarchy. - -\item[\refqualclass{checker/modifiability/qual}{IteratorPreservesRemove}] -Calling \code{iterator()} on this collection preserves the receiver's -ability to remove elements: if the collection is \<@Shrinkable>, then -the resulting \code{Iterator} is \<@Shrinkable> and its -\code{remove} method may be called. +\item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPolyShrink}] +The top qualifier. +The checker cannot determine whether this collection's \code{iterator()} is +\<@PolyShrink>. +This is the default qualifier for unannotated types. + +\item[\refqualclass{checker/modifiability/qual}{IteratorPolyShrink}] +This collection's \code{iterator()} is \<@PolyShrink>. That is, if +collection \ is \<@Shrinkable>, then \ is also +\<@Shrinkable>. \end{description} @@ -200,43 +245,6 @@ subtype of \code{@Growable @Shrinkable @Replaceable List} (the Replace hierarchy differs). -\subsectionAndLabel{Alias annotations}{modifiability-aliases} - -For convenience, the Modifiability Checker provides four alias -annotations. They are not part of any hierarchy; the checker expands -each one into its constituent qualifiers when it appears in source code. - -\begin{description} - -\item[\refqualclass{checker/modifiability/qual}{Modifiable}] -Alias for \code{@Growable @Shrinkable @Replaceable}. -Calling any mutating operation on this collection will not throw -\code{UnsupportedOperationException}. - -\item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] -Alias for \code{@Ungrowable @Unshrinkable @Unreplaceable}. -The collection is definitely unmodifiable: calling any mutating operation -always throws \code{UnsupportedOperationException}. - -\item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] -Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. -The checker cannot determine the modifiability of the collection. - -\item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] -Alias for \code{@PolyGrow @PolyShrink @PolyReplace} -(see Section~\ref{modifiability-polymorphic}). - -\end{description} - -\noindent -Examples using alias annotations: - -\begin{Verbatim} - @Modifiable List mod = new ArrayList<>(); // all three capabilities - @Unmodifiable List unmod = List.of("a"); // no capabilities - @UnknownModifiability List unknown = ...; // capability is unknown -\end{Verbatim} - \subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} A polymorphic qualifier specifies that a method's return type has the @@ -247,19 +255,19 @@ \item[\refqualclass{checker/modifiability/qual}{PolyGrow}] Polymorphic qualifier for the Grow hierarchy. -The return type's Grow qualifier matches the Grow qualifier of whichever +The return type's growability matches the growability of whichever formal parameter is annotated with \<@PolyGrow>. \item[\refqualclass{checker/modifiability/qual}{PolyShrink}] Polymorphic qualifier for the Shrink hierarchy. The return type's Shrink qualifier matches the Shrink qualifier of whichever formal parameter is annotated with \<@PolyShrink>. -Useful for methods such as \code{Map.keySet()} that preserve Shrink while +Useful for methods such as \code{Map.keySet()} that preserve shrinkability while returning an \<@Ungrowable> view. \item[\refqualclass{checker/modifiability/qual}{PolyReplace}] Polymorphic qualifier for the Replace hierarchy. -The return type's Replace qualifier matches the Replace qualifier of +The return type's replaceability matches the replaceability of whichever formal parameter is annotated with \<@PolyReplace>. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] @@ -275,8 +283,7 @@ polymorphic and concrete qualifiers across hierarchies. For example: \begin{Verbatim} - // Shrink is preserved from the map; result is explicitly @Ungrowable. - @PolyShrink @Ungrowable Set keySet(@PolyShrink Map map) + @Ungrowable @PolyShrink Set keySet(@PolyShrink Map map) \end{Verbatim} If the map is \code{@Growable @Shrinkable @Replaceable}, then @@ -293,15 +300,14 @@ \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} The Modifiability Checker supports one annotation that specifies method -behavior. This is a declaration annotation, not a type annotation: it -applies to the method itself rather than to some particular type. +behavior. \begin{description} \item[\refqualclass{checker/modifiability/qual}{ThrowsUOE}] -This is a declaration annotation (not a type qualifier) that is applied to -method and constructor declarations. It indicates that the method always throws -\code{UnsupportedOperationException}. (Therefore, any code that calls it is +When written on a +method or constructor declaration, it indicates that the method always throws +\UnsupportedOperationException. (Therefore, any code that calls it is erroneous.) The Modifiability Checker issues an error at every call to a method annotated with \code{@ThrowsUOE}. @@ -309,7 +315,35 @@ \sectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} -Some Java types lack methods for certain kinds of mutation. The +Some Java types lack methods for certain kinds of mutation. +For example, \sunjavadoc{java.base/java/util/Queue.html}{\} has +\ and \ methods but no replacement methods. However, the type +\<@Replaceable Queue> still makes sense; for example, it is the type of +\ in \. + +In general, a type is given the top qualifier when it has no method that +performs the given mutation. For example, \ defaults to +\<@UnknownReplace>. (In the above assignment, \ was flow-sensitively +refined to have type \<@Replaceable>. + +This behavior ensures that code like this type-checks: + +\begin{Verbatim} +@Growable LinkedList list = ...; +Queue q = list; +@Growable LinkedList list2 = ...; // still @Growable +\end{Verbatim} + + +java.base/java/util/Collections.html\#unmodifiableMap(java.util.Map)}{\code{Collections.unmodifiableMap()}} + + +Queue has no replace methods in general + +When a + + +The Modifiability Checker automatically removes capabilities that the type cannot support. @@ -366,16 +400,20 @@ \sectionAndLabel{Iterator Behavior}{modifiability-iterator-behavior} +By default, calling \ returns an \<@Unshrinkable +Iterator>. However + + The iterator returned by a shrinkable collection does not always support \code{Iterator.remove()}. Therefore, when \code{iterator()} is called on an \<@Shrinkable> receiver, the result is \<@Shrinkable> only if the -receiver is also \<@IteratorPreservesRemove>. Otherwise, the result's +receiver is also \<@IteratorPolyShrink>. Otherwise, the result's Shrink qualifier is conservatively \<@UnknownShrink>. When \code{iterator()} is called on an \<@Unshrinkable> receiver, the result is \<@Unshrinkable>. \begin{Verbatim} - @IteratorPreservesRemove @Shrinkable List list = ...; + @IteratorPolyShrink @Shrinkable List list = ...; @Shrinkable Iterator it = list.iterator(); it.remove(); // OK @@ -455,4 +493,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace -% LocalWords: UnknownIteratorPreservesRemove IteratorPreservesRemove +% LocalWords: UnknownIteratorPolyShrink IteratorPolyShrink From 20f03d1f7ae5b9dac7d8b21953f3a4136027373f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 2 May 2026 14:17:00 -0700 Subject: [PATCH 147/308] Update manual --- docs/manual/modifiability-checker.tex | 135 ++++++++------------------ 1 file changed, 43 insertions(+), 92 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 417eb7f16ec3..f954d65ea369 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -60,7 +60,7 @@ \end{itemize} \noindent -Some collections can also be modified through their iterator. +Some collections can also be modified through their iterator; see Section~\ref{modifiability-iterator-behavior}. To run the Modifiability Checker: @@ -99,8 +99,8 @@ \end{figure} Figure~\ref{fig-modifiability-lattice} shows the hierarchies. -An assignment \code{T x = y} is valid only if the compile-type of \code{y} is a -subtype of (or equal to) \code{T} in \emph{every} hierarchy simultaneously. For +An assignment \code{T x = y} is valid only if the compile-time type of \code{y} is a +subtype of (or equal to) \code{T} in every hierarchy simultaneously. For example, \code{@Growable @UnknownShrink @UnknownReplace List} is a subtype of \code{@UnknownGrow @UnknownShrink @UnknownReplace List} (because \<@Growable> $\le$ \<@UnknownGrow>), but it is \emph{not} a @@ -117,16 +117,17 @@ \begin{description} \item[\refqualclass{checker/modifiability/qual}{Modifiable}] -Alias for \code{@Growable @Shrinkable @Replaceable} (but see XXXXX for exceptions). -Calling any mutating operation on this collection will not throw +Alias for \code{@Growable @Shrinkable @Replaceable} (but see +Section~\ref{modifiability-types-lacking-methods} for exceptions). Calling +any mutating operation on this collection will not throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] -Alias for \code{@Ungrowable @Unshrinkable @Unreplaceable} (but see XXXXX for exceptions). -The collection is definitely unmodifiable: calling any mutating operation -always throws \UnsupportedOperationException. -The checker issues a warning at every invocation of a grow, -shrink, or replace operation. +Alias for \code{@Ungrowable @Unshrinkable @Unreplaceable} (but see +Section~\ref{modifiability-types-lacking-methods} for exceptions). The +collection is definitely unmodifiable: calling any mutating operation +always throws \UnsupportedOperationException. The checker issues a warning +at every invocation of a grow, shrink, or replace operation. \item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. @@ -146,7 +147,7 @@ \begin{Verbatim} @Modifiable List mod = new ArrayList<>(); // all three capabilities @Unmodifiable List unmod = List.of("a"); // no capabilities - @UnknownModifiability List unknown = ...; // capability is unknown + @UnknownModifiability List unknown = ...; // capabilities are unknown at compile time \end{Verbatim} \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} @@ -240,10 +241,11 @@ Because the three hierarchies are independent, subtyping checks each hierarchy individually. A \code{@Growable @Shrinkable List} is a -subtype of \code{@Growable List} (Shrink information is dropped, Replace +subtype of \code{@Growable List} (Shrink defaults to \<@UnknownGrow> on the +right, Replace defaults to \<@UnknownReplace> on both sides), but it is \emph{not} a -subtype of \code{@Growable @Shrinkable @Replaceable List} (the Replace -hierarchy differs). +subtype of \code{@Growable @Shrinkable @Replaceable List} (due to the Replace +qualifiers). \subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} @@ -307,24 +309,27 @@ \item[\refqualclass{checker/modifiability/qual}{ThrowsUOE}] When written on a method or constructor declaration, it indicates that the method always throws -\UnsupportedOperationException. (Therefore, any code that calls it is -erroneous.) The Modifiability Checker issues an error at every call to a +\UnsupportedOperationException. Therefore, any code that calls it is +erroneous. The Modifiability Checker issues an error at every call to a method annotated with \code{@ThrowsUOE}. \end{description} -\sectionAndLabel{Types without grow, shrink, and/or replace methods}{types-without-full-gsr} +\sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} Some Java types lack methods for certain kinds of mutation. -For example, \sunjavadoc{java.base/java/util/Queue.html}{\} has +For example, \sunjavadoc{java.base/java/util/Queue.html}{\code{Queue}} has \ and \ methods but no replacement methods. However, the type \<@Replaceable Queue> still makes sense; for example, it is the type of \ in \. -In general, a type is given the top qualifier when it has no method that -performs the given mutation. For example, \ defaults to -\<@UnknownReplace>. (In the above assignment, \ was flow-sensitively -refined to have type \<@Replaceable>. +When a programmer writes \<@Modifiable> or \<@Unmodifiable> on a type that +has no method for a given mutation, the Modifiability Checker uses the top +qualifier. For example, both \<@Modifiable Queue> and \<@Unmodifiable +Queue> have \<@UnknownReplace> as their replaceability qualifier. (In the +above assignment, \ was flow-sensitively refined to have type +\<@Replaceable>.) Other examples are that \ has no replace methods +and \ has no grow or replace methods. This behavior ensures that code like this type-checks: @@ -335,82 +340,28 @@ \end{Verbatim} -java.base/java/util/Collections.html\#unmodifiableMap(java.util.Map)}{\code{Collections.unmodifiableMap()}} - - -Queue has no replace methods in general - -When a - - -The -Modifiability Checker automatically removes capabilities that the type -cannot support. - -\paragraphAndLabel{Set and Queue (except LinkedList).}{modifiability-set-queue} -The \code{Set} interface has no replace methods (\code{set} is a -\code{List}-only operation). Similarly, \code{Queue} has no replace -methods in general. The Modifiability Checker therefore sets the -Replace hierarchy qualifier to \<@UnknownReplace> for any \code{Set} or -non-\code{LinkedList} \code{Queue}, regardless of what is written on the -type. For example: - -\begin{itemize} - \item \code{@Modifiable Set} $\Rightarrow$ - treated as \code{@Growable @Shrinkable @UnknownReplace Set} - \item \code{@Unmodifiable Set} $\Rightarrow$ - treated as \code{@Ungrowable @Unshrinkable @UnknownReplace Set} - \item \code{@Growable @Replaceable Set} $\Rightarrow$ - treated as \code{@Growable @UnknownShrink @UnknownReplace Set} - \item \code{@Growable @Shrinkable Set} $\Rightarrow$ unchanged - (\<@UnknownReplace> is already the default) -\end{itemize} - -\paragraphAndLabel{Map.Entry.}{modifiability-map-entry} -\code{Map.Entry} has only the optional \code{setValue} method (a -replace operation) and no grow or shrink operations. The Modifiability -Checker therefore sets the Grow and Shrink hierarchy qualifiers to -\<@UnknownGrow> and \<@UnknownShrink> respectively. For example: - -\begin{itemize} - \item \code{Map.@Modifiable Entry} $\Rightarrow$ - treated as \code{Map.@UnknownGrow @UnknownShrink @Replaceable Entry} - \item \code{Map.@Unmodifiable Entry} $\Rightarrow$ - treated as \code{Map.@UnknownGrow @UnknownShrink @Unreplaceable Entry} - \item \code{Map.@Growable Entry} $\Rightarrow$ - treated as \code{Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry} -\end{itemize} +\sectionAndLabel{Behavior of the \ method}{modifiability-iterator-behavior} -\paragraphAndLabel{Iterator.}{modifiability-iterator} -\code{Iterator} has only the optional \code{remove} method (a shrink -operation) and no grow or replace operations. (Note that -\code{ListIterator} is treated normally because it also has optional -\code{add} and \code{set} operations.) The Modifiability Checker -therefore sets the Grow and Replace hierarchy qualifiers to -\<@UnknownGrow> and \<@UnknownReplace>: +By default, calling \ returns an \<@Unshrinkable +Iterator>; that is, the signature of \ is -\begin{itemize} - \item \code{@Modifiable Iterator} $\Rightarrow$ - treated as \code{@UnknownGrow @Shrinkable @UnknownReplace Iterator} - \item \code{@Unmodifiable Iterator} $\Rightarrow$ - treated as \code{@UnknownGrow @Unshrinkable @UnknownReplace Iterator} - \item \code{@Growable Iterator} $\Rightarrow$ - treated as \code{@UnknownGrow @UnknownShrink @UnknownReplace Iterator} -\end{itemize} +\begin{Verbatim} +@Unshrinkable Iterator iterator() +\end{Verbatim} -\sectionAndLabel{Iterator Behavior}{modifiability-iterator-behavior} +However, some subclasses of \ can return a shrinkable +iterator. For such subclasses of \, the signature of +\ is -By default, calling \ returns an \<@Unshrinkable -Iterator>. However +\begin{Verbatim} +@PolyShrink Iterator iterator(@PolyShrink MySubclassOfCollection this) +\end{Verbatim} +A collection uses the second signature if the collection is +\<@IteratorPolyShrink>. The collection uses the first signature if the +collection is \<@UnknownIteratorPolyShrink>. -The iterator returned by a shrinkable collection does not always support -\code{Iterator.remove()}. Therefore, when \code{iterator()} is called on -an \<@Shrinkable> receiver, the result is \<@Shrinkable> only if the -receiver is also \<@IteratorPolyShrink>. Otherwise, the result's -Shrink qualifier is conservatively \<@UnknownShrink>. When -\code{iterator()} is called on an \<@Unshrinkable> receiver, the result -is \<@Unshrinkable>. +Here are examples of its use: \begin{Verbatim} @IteratorPolyShrink @Shrinkable List list = ...; From 229718273b39ce48f70b2c4bec459f027302a6b3 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 2 May 2026 14:59:44 -0700 Subject: [PATCH 148/308] fixed typos in javadoc --- .../checker/modifiability/qual/Modifiable.java | 4 ++-- .../checker/modifiability/qual/UnknownIteratorPolyShrink.java | 2 +- .../checker/modifiability/qual/Unmodifiable.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 537f47bfada7..dd916fdfd088 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -12,8 +12,8 @@ * {@link UnsupportedOperationException}. * *

As an exception, {@code @Modifiable} means {@code @Unknown*} if the type does not support a - * given category of operation; for example, {@Modifiable Iterator} means - * {@code @UnknownGrow @UnknownShrink @Replaceable Iterator}.) + * given category of operation; for example, {@code @Modifiable Iterator} means + * {@code @UnknownGrow @UnknownShrink @Replaceable Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java index b74f7b26c438..09a44469916b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java @@ -13,7 +13,7 @@ * *

This is the default qualifier for unannotated types. * - * @see #IteratorPolyShrink + * @see IteratorPolyShrink * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 56f430ac40ca..870ad74afa29 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -12,8 +12,8 @@ * UnsupportedOperationException}. * *

As an exception, {@code @Unmodifiable} means {@code @Unknown*} if the type does not support a - * given category of operation; for example, {@Unmodifiable Iterator} means - * {@code @UnknownGrow @UnknownShrink @Unreplaceable Iterator}.) + * given category of operation; for example, {@code @Unmodifiable Iterator} means + * {@code @UnknownGrow @UnknownShrink @Unreplaceable Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ From 6c22d0582f353a163b7c290987e8506cb5592c81 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 2 May 2026 15:29:04 -0700 Subject: [PATCH 149/308] clean up methodFromUse in ShrinkAnnotatedTypeFactory and add documentations. --- .../shrink/ShrinkAnnotatedTypeFactory.java | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 42714636ac6b..736803b94075 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -129,19 +129,41 @@ protected ParameterizedExecutableType methodFromUse( refineIteratorReturnType(tree, method); } - if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { - return mType; + if (ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { + refineCollectionsPlumeWithoutDuplicatesReturnType(tree, method); } - AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); - if (argumentType.hasPrimaryAnnotation(SHRINKABLE)) { - method.getReturnType().replaceAnnotation(SHRINKABLE); - } else { - method.getReturnType().replaceAnnotation(UNKNOWN_SHRINK); - } return mType; } + /** + * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. + * + *

{@code withoutDuplicates} has a conditional aliasing contract: it may return its argument + * when the argument is already a List with no duplicates, but otherwise it returns a new + * ArrayList. A stub annotation like + * + *

{@code
+   * static  @PolyShrink List withoutDuplicates(@PolyShrink Collection values)
+   * }
+ * + * would be unsound, because an {@code @Unshrinkable} input could receive the fresh ArrayList, + * whose static type should be {@code @Shrinkable}. It would also be too imprecise to always use + * {@code @UnknownShrink}, because passing a {@code @Shrinkable} collection guarantees that both + * possible results are shrinkable. Therefore, model the method here as preserving + * {@code @Shrinkable} inputs and otherwise returning {@code @UnknownShrink}. + * + * @param tree the invocation of {@code withoutDuplicates} + * @param methodType the annotated executable type of the invoked method + */ + private void refineCollectionsPlumeWithoutDuplicatesReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); + AnnotationMirror returnAnnotation = + argumentType.hasPrimaryAnnotation(SHRINKABLE) ? SHRINKABLE : UNKNOWN_SHRINK; + methodType.getReturnType().replaceAnnotation(returnAnnotation); + } + /** * Refines {@code iterator()} return type based on {@code @IteratorPolyShrink}. * From a7c8b4bb21de65e6d8b5dd342a3dce34b4b1f084 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 2 May 2026 17:33:40 -0700 Subject: [PATCH 150/308] extends special treatment of iterator method to ListIterator. TODO: add this to grow and replace. add a test case to check whether TypeAnnotator preserve types Co-authored-by: Copilot --- .../shrink/ShrinkAnnotatedTypeFactory.java | 16 +++++--- .../modifiability/IteratorPrecisionTest.java | 25 ++++++++++++ .../SubtypeCapabilityPreservationTest.java | 39 +++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 checker/tests/modifiability/SubtypeCapabilityPreservationTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 736803b94075..bb3761f2b109 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -209,20 +209,26 @@ private void refineIteratorReturnType( } /** - * Returns true if this invocation is an instance {@code iterator()} method returning Iterator. + * Returns true if this invocation is an iterator-returning {@code iterator()} or {@code + * listIterator()} method. * * @param tree the method invocation to test * @param methodType the annotated executable type of the invoked method - * @return true if this invocation is an instance {@code iterator()} method returning Iterator + * @return true if this invocation returns an Iterator from {@code iterator()} or {@code + * listIterator()} */ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableType methodType) { - if (!tree.getArguments().isEmpty()) { + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null) { return false; } - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null || !invokedMethod.getSimpleName().contentEquals("iterator")) { + String methodName = invokedMethod.getSimpleName().toString(); + int argCount = tree.getArguments().size(); + if (!((methodName.equals("iterator") && argCount == 0) + || (methodName.equals("listIterator") && argCount <= 1))) { return false; } + TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); } diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index f02283457b67..c84f9e2dd4f3 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -11,6 +11,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; @@ -76,6 +77,30 @@ void copyOnWriteArrayListIterator() { iterator2.remove(); } + void copyOnWriteArrayListListIterator() { + CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); + @Unmodifiable ListIterator iterator = list.listIterator(); + @Unmodifiable ListIterator indexedIterator = list.listIterator(0); + + List list2 = new CopyOnWriteArrayList<>(); + // default to be unknown mod. + ListIterator iterator2 = list2.listIterator(); + ListIterator indexedIterator2 = list2.listIterator(0); + } + + void arryaListListIterator() { + ArrayList list = new ArrayList<>(); + @Modifiable ListIterator iterator = list.listIterator(); + iterator.remove(); + iterator.set("c"); + iterator.add("d"); + @Modifiable ListIterator indexedIterator = list.listIterator(0); + + List list2 = new ArrayList<>(); + @Modifiable ListIterator iterator2 = list2.listIterator(); + @Modifiable ListIterator indexedIterator2 = list2.listIterator(0); + } + void UnmodListIterator() { List list = List.of("a", "b"); @Unshrinkable Iterator iterator = list.iterator(); diff --git a/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java new file mode 100644 index 000000000000..59de0a98ce14 --- /dev/null +++ b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java @@ -0,0 +1,39 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.ListIterator; +import java.util.Queue; +import org.checkerframework.checker.modifiability.qual.Modifiable; + +// Tests that storing a modifiable subtype in a less-capable supertype does not permanently discard +// modifiability dimensions that the subtype supports. +class SubtypeCapabilityPreservationTest { + + void listIteratorThroughIterator() { + @Modifiable ListIterator listIterator = new LinkedList().listIterator(); + @Modifiable Iterator iterator = listIterator; + @Modifiable ListIterator roundTrip = (ListIterator) iterator; + roundTrip.add("added"); + roundTrip.set("replaced"); + roundTrip.remove(); + } + + void linkedListThroughQueue() { + @Modifiable LinkedList linkedList = new LinkedList<>(); + @Modifiable Queue queue = linkedList; + @Modifiable LinkedList roundTrip = (LinkedList) queue; + roundTrip.add("added"); + roundTrip.set(0, "replaced"); + roundTrip.remove(); + } + + void arrayListThroughCollection() { + @Modifiable ArrayList arrayList = new ArrayList<>(); + @Modifiable Collection collection = arrayList; + @Modifiable ArrayList roundTrip = (ArrayList) collection; + roundTrip.add("added"); + roundTrip.set(0, "replaced"); + roundTrip.remove(0); + } +} From 4a17235bbf2b45d8d6150d69a3313c9da1e58ea0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 3 May 2026 19:14:16 -0700 Subject: [PATCH 151/308] add listiterator handler in grow and replace. --- .../grow/GrowAnnotatedTypeFactory.java | 113 ++++++++++++++++- .../replace/ReplaceAnnotatedTypeFactory.java | 116 ++++++++++++++++-- 2 files changed, 215 insertions(+), 14 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 095fd7d6b4c8..f382ea2c0a0b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -1,21 +1,25 @@ package org.checkerframework.checker.modifiability.grow; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomGrow; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrow; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.UnknownGrow; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -28,6 +32,8 @@ import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link GrowChecker}. */ @@ -56,6 +62,12 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyGrow} qualifier. */ private final AnnotationMirror POLY_GROW; + /** The {@code @}{@link UnknownIteratorPolyShrink} qualifier. */ + private final AnnotationMirror UNKNOWN_ITER; + + /** The {@code @}{@link IteratorPolyShrink} qualifier. */ + private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; + /** * Creates a GrowAnnotatedTypeFactory. * @@ -77,6 +89,10 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); + this.UNKNOWN_ITER = + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyShrink.class); + this.ITERATOR_PRESERVE_REMOVE = + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrink.class); addAliasedTypeAnnotation(Modifiable.class, GROWABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNGROWABLE); @@ -89,7 +105,13 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - UnknownGrow.class, Growable.class, Ungrowable.class, BottomGrow.class, PolyGrow.class)); + UnknownGrow.class, + Growable.class, + Ungrowable.class, + BottomGrow.class, + PolyGrow.class, + UnknownIteratorPolyShrink.class, + IteratorPolyShrink.class)); } @Override @@ -101,11 +123,16 @@ protected TypeAnnotator createTypeAnnotator() { protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + AnnotatedExecutableType method = mType.executableType(); + + if (isListIteratorMethod(tree, method)) { + refineListIteratorReturnType(tree, method); + } + if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { return mType; } - AnnotatedExecutableType method = mType.executableType(); AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(GROWABLE)) { method.getReturnType().replaceAnnotation(GROWABLE); @@ -115,6 +142,79 @@ protected ParameterizedExecutableType methodFromUse( return mType; } + /** + * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrink}. + * + * @param tree the listIterator method invocation + * @param methodType the annotated executable type of the invoked method + */ + private void refineListIteratorReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + AnnotatedTypeMirror returnType = methodType.getReturnType(); + if (returnType.hasPrimaryAnnotation(UNGROWABLE) + || returnType.hasPrimaryAnnotation(GROWABLE) + || returnType.hasPrimaryAnnotation(POLY_GROW)) { + return; + } + + Tree receiverTree = TreeUtils.getReceiverTree(tree); + if (receiverTree == null) { + return; + } + AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + + if (receiverType.hasPrimaryAnnotation(UNGROWABLE)) { + returnType.replaceAnnotation(UNGROWABLE); + return; + } + + if (!receiverType.hasPrimaryAnnotation(GROWABLE)) { + return; + } + + if (hasIteratorPolyShrink(receiverType)) { + returnType.replaceAnnotation(GROWABLE); + } else { + returnType.replaceAnnotation(UNKNOWN_GROW); + } + } + + /** + * Returns true if this invocation is a {@code listIterator()} method that returns an Iterator. + * + * @param tree the method invocation to test + * @param methodType the annotated executable type of the invoked method + * @return true if this invocation returns an Iterator from {@code listIterator()} + */ + private boolean isListIteratorMethod( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null) { + return false; + } + if (!invokedMethod.getSimpleName().contentEquals("listIterator") + || tree.getArguments().size() > 1) { + return false; + } + + TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); + return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); + } + + /** + * Returns true if {@code type} has the {@code @IteratorPolyShrink} marker annotation. + * + * @param type the type to test + * @return true if {@code type} has the {@code @IteratorPolyShrink} marker annotation + */ + private boolean hasIteratorPolyShrink(AnnotatedTypeMirror type) { + if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + return true; + } + return AnnotationUtils.containsSameByClass( + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrink.class); + } + /** * Removes grow capability for types that structurally cannot support it: * @@ -162,7 +262,8 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { @Override protected QualifierUpperBounds createQualifierUpperBounds() { return new QualifierUpperBounds(this) { - private final AnnotationMirrorSet unknownGrow = AnnotationMirrorSet.singleton(UNKNOWN_GROW); + private final AnnotationMirrorSet unknownGrowAndIter = + AnnotationMirrorSet.unmodifiableSet(Arrays.asList(UNKNOWN_GROW, UNKNOWN_ITER)); @Override public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { @@ -171,9 +272,11 @@ public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { // Standard Iterator (not ListIterator) cannot be grown. - return unknownGrow; + return unknownGrowAndIter; } - return super.getBoundQualifiers(type); + AnnotationMirrorSet bounds = new AnnotationMirrorSet(super.getBoundQualifiers(type)); + bounds.add(UNKNOWN_ITER); + return bounds; } }; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index b7c3a028763b..c1b349a08ccf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -1,19 +1,23 @@ package org.checkerframework.checker.modifiability.replace; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplace; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplace; import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyShrink; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownReplace; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -28,6 +32,8 @@ import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ReplaceChecker}. */ @@ -65,6 +71,12 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyReplace} qualifier. */ private final AnnotationMirror POLY_REPLACE; + /** The {@code @}{@link UnknownIteratorPolyShrink} qualifier. */ + private final AnnotationMirror UNKNOWN_ITER; + + /** The {@code @}{@link IteratorPolyShrink} qualifier. */ + private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; + /** * Creates a ReplaceAnnotatedTypeFactory. * @@ -91,6 +103,10 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); + this.UNKNOWN_ITER = + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyShrink.class); + this.ITERATOR_PRESERVE_REMOVE = + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrink.class); addAliasedTypeAnnotation(Modifiable.class, REPLACEABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNREPLACEABLE); @@ -107,7 +123,9 @@ protected Set> createSupportedTypeQualifiers() { Replaceable.class, Unreplaceable.class, BottomReplace.class, - PolyReplace.class)); + PolyReplace.class, + UnknownIteratorPolyShrink.class, + IteratorPolyShrink.class)); } @Override @@ -119,11 +137,16 @@ protected TypeAnnotator createTypeAnnotator() { protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + AnnotatedExecutableType method = mType.executableType(); + + if (isListIteratorMethod(tree, method)) { + refineListIteratorReturnType(tree, method); + } + if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { return mType; } - AnnotatedExecutableType method = mType.executableType(); AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(REPLACEABLE)) { method.getReturnType().replaceAnnotation(REPLACEABLE); @@ -133,6 +156,79 @@ protected ParameterizedExecutableType methodFromUse( return mType; } + /** + * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrink}. + * + * @param tree the listIterator method invocation + * @param methodType the annotated executable type of the invoked method + */ + private void refineListIteratorReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + AnnotatedTypeMirror returnType = methodType.getReturnType(); + if (returnType.hasPrimaryAnnotation(UNREPLACEABLE) + || returnType.hasPrimaryAnnotation(REPLACEABLE) + || returnType.hasPrimaryAnnotation(POLY_REPLACE)) { + return; + } + + Tree receiverTree = TreeUtils.getReceiverTree(tree); + if (receiverTree == null) { + return; + } + AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + + if (receiverType.hasPrimaryAnnotation(UNREPLACEABLE)) { + returnType.replaceAnnotation(UNREPLACEABLE); + return; + } + + if (!receiverType.hasPrimaryAnnotation(REPLACEABLE)) { + return; + } + + if (hasIteratorPolyShrink(receiverType)) { + returnType.replaceAnnotation(REPLACEABLE); + } else { + returnType.replaceAnnotation(UNKNOWN_REPLACE); + } + } + + /** + * Returns true if this invocation is a {@code listIterator()} method that returns an Iterator. + * + * @param tree the method invocation to test + * @param methodType the annotated executable type of the invoked method + * @return true if this invocation returns an Iterator from {@code listIterator()} + */ + private boolean isListIteratorMethod( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null) { + return false; + } + if (!invokedMethod.getSimpleName().contentEquals("listIterator") + || tree.getArguments().size() > 1) { + return false; + } + + TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); + return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); + } + + /** + * Returns true if {@code type} has the {@code @IteratorPolyShrink} marker annotation. + * + * @param type the type to test + * @return true if {@code type} has the {@code @IteratorPolyShrink} marker annotation + */ + private boolean hasIteratorPolyShrink(AnnotatedTypeMirror type) { + if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + return true; + } + return AnnotationUtils.containsSameByClass( + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrink.class); + } + /** * Removes capabilities that cannot be supported by structural constraints of the collection type: * @@ -188,30 +284,32 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void p) { @Override protected QualifierUpperBounds createQualifierUpperBounds() { return new QualifierUpperBounds(this) { - private final AnnotationMirrorSet unknownReplace = - AnnotationMirrorSet.singleton(UNKNOWN_REPLACE); + private final AnnotationMirrorSet unknownReplaceAndIter = + AnnotationMirrorSet.unmodifiableSet(Arrays.asList(UNKNOWN_REPLACE, UNKNOWN_ITER)); @Override public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { TypeMirror erasedType = types.erasure(type); if (types.isSameType(erasedType, collectionErasure)) { // Elements of a raw Collection are treated as @UnknownReplace. - return unknownReplace; + return unknownReplaceAndIter; } else if (TypesUtils.isErasedSubtype(type, setErasure, types)) { // Elements of a set can never be replaced, so treat them as @UnknownReplace. Even if // they are annotation @Modifiable in a stubfile. - return unknownReplace; + return unknownReplaceAndIter; } else if (TypesUtils.isErasedSubtype(type, queueErasure, types) && !TypesUtils.isErasedSubtype(type, linkedListErasure, types)) { // Elements of a queue (but not LinkedList) can never be replaced, so treat them as // @UnknownReplace. - return unknownReplace; + return unknownReplaceAndIter; } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { // Iterators cannot replace elements. - return unknownReplace; + return unknownReplaceAndIter; } - return super.getBoundQualifiers(type); + AnnotationMirrorSet bounds = new AnnotationMirrorSet(super.getBoundQualifiers(type)); + bounds.add(UNKNOWN_ITER); + return bounds; } }; } From ffcfd9f2b3d9eef6ac7b308c2eac679b85278bad Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 3 May 2026 20:58:53 -0700 Subject: [PATCH 152/308] reorganize methodFromUse for both grow and replace annotatedTypeFactory. --- .../grow/GrowAnnotatedTypeFactory.java | 34 ++++++++++++++++--- .../replace/ReplaceAnnotatedTypeFactory.java | 34 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index f382ea2c0a0b..693be47960b7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -129,17 +129,41 @@ protected ParameterizedExecutableType methodFromUse( refineListIteratorReturnType(tree, method); } - if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { - return mType; + if (ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { + refineCollectionsPlumeWithoutDuplicatesReturnType(tree, method); } + return mType; + } + + /** + * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. + * + *

{@code withoutDuplicates} has a conditional aliasing contract: it may return its argument + * when the argument is already a List with no duplicates, but otherwise it returns a new + * ArrayList. A stub annotation like + * + *

{@code
+   * static  @PolyGrow List withoutDuplicates(@PolyGrow Collection values)
+   * }
+ * + * would be unsound, because an {@code @Ungrowable} input could receive the fresh ArrayList, whose + * static type should be {@code @Growable}. It would also be too imprecise to always use + * {@code @UnknownGrow}, because passing a {@code @Growable} collection guarantees that both + * possible results are growable. Therefore, model the method here as preserving {@code @Growable} + * inputs and otherwise returning {@code @UnknownGrow}. + * + * @param tree the invocation of {@code withoutDuplicates} + * @param methodType the annotated executable type of the invoked method + */ + private void refineCollectionsPlumeWithoutDuplicatesReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(GROWABLE)) { - method.getReturnType().replaceAnnotation(GROWABLE); + methodType.getReturnType().replaceAnnotation(GROWABLE); } else { - method.getReturnType().replaceAnnotation(UNKNOWN_GROW); + methodType.getReturnType().replaceAnnotation(UNKNOWN_GROW); } - return mType; } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index c1b349a08ccf..419844f1be7d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -143,17 +143,41 @@ protected ParameterizedExecutableType methodFromUse( refineListIteratorReturnType(tree, method); } - if (!ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { - return mType; + if (ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { + refineCollectionsPlumeWithoutDuplicatesReturnType(tree, method); } + return mType; + } + + /** + * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. + * + *

{@code withoutDuplicates} has a conditional aliasing contract: it may return its argument + * when the argument is already a List with no duplicates, but otherwise it returns a new + * ArrayList. A stub annotation like + * + *

{@code
+   * static  @PolyReplace List withoutDuplicates(@PolyReplace Collection values)
+   * }
+ * + * would be unsound, because an {@code @Unreplaceable} input could receive the fresh ArrayList, + * whose static type should be {@code @Replaceable}. It would also be too imprecise to always use + * {@code @UnknownReplace}, because passing a {@code @Replaceable} collection guarantees that both + * possible results are replaceable. Therefore, model the method here as preserving + * {@code @Replaceable} inputs and otherwise returning {@code @UnknownReplace}. + * + * @param tree the invocation of {@code withoutDuplicates} + * @param methodType the annotated executable type of the invoked method + */ + private void refineCollectionsPlumeWithoutDuplicatesReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(REPLACEABLE)) { - method.getReturnType().replaceAnnotation(REPLACEABLE); + methodType.getReturnType().replaceAnnotation(REPLACEABLE); } else { - method.getReturnType().replaceAnnotation(UNKNOWN_REPLACE); + methodType.getReturnType().replaceAnnotation(UNKNOWN_REPLACE); } - return mType; } /** From d34d79471ec4b50f84d7c2b7a3a082526c0908b0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 3 May 2026 21:34:18 -0700 Subject: [PATCH 153/308] rename IteratorPolyShrink to IteratorPolyMod --- .../afu/scenelib/util/coll/WrapperMap.java | 4 +-- buildSrc/src/main/groovy/cf-spotless.gradle | 4 +-- ...orPolyShrink.java => IteratorPolyMod.java} | 12 +++---- ...hrink.java => UnknownIteratorPolyMod.java} | 4 +-- .../grow/GrowAnnotatedTypeFactory.java | 28 ++++++++-------- .../replace/ReplaceAnnotatedTypeFactory.java | 28 ++++++++-------- .../shrink/ShrinkAnnotatedTypeFactory.java | 33 +++++++++---------- .../modifiability/IteratorPrecisionTest.java | 14 ++++---- docs/manual/modifiability-checker.tex | 16 ++++----- 9 files changed, 71 insertions(+), 72 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{IteratorPolyShrink.java => IteratorPolyMod.java} (53%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownIteratorPolyShrink.java => UnknownIteratorPolyMod.java} (91%) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 126096f425c3..8784553f7282 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,7 +3,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyShrink; /** @@ -39,7 +39,7 @@ public boolean containsValue(Object value) { } @Override - public @IteratorPolyShrink @PolyShrink Set> entrySet( + public @IteratorPolyMod @PolyShrink Set> entrySet( @PolyShrink WrapperMap this) { return back.entrySet(); } diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 79be8ccc759b..0fc1bff209c4 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -96,7 +96,7 @@ spotless { .addTypeAnnotation("BottomReplace") .addTypeAnnotation("BottomShrink") .addTypeAnnotation("Growable") - .addTypeAnnotation("IteratorPolyShrink") + .addTypeAnnotation("IteratorPolyMod") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrow") .addTypeAnnotation("PolyModifiable") @@ -106,7 +106,7 @@ spotless { .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") .addTypeAnnotation("UnknownGrow") - .addTypeAnnotation("UnknownIteratorPolyShrink") + .addTypeAnnotation("UnknownIteratorPolyMod") .addTypeAnnotation("UnknownModifiability") .addTypeAnnotation("UnknownReplace") .addTypeAnnotation("UnknownShrink") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java similarity index 53% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index 164a8f8d1308..d5d8ef9253b7 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -8,10 +8,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * If a collection's type is {@code @IteratorPolyShrink}, then its {@code iterator()} method - * preserves the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has - * type {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any - * collection whose type is {@code @UnknownIteratorPolyShrink}, its iterator is always + * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} method preserves + * the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has type + * {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any + * collection whose type is {@code @UnknownIteratorPolyMod}, its iterator is always * {@code @Unshrinkable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownIteratorPolyShrink.class) -public @interface IteratorPolyShrink {} +@SubtypeOf(UnknownIteratorPolyMod.class) +public @interface IteratorPolyMod {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyMod.java similarity index 91% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyMod.java index 09a44469916b..4c359545002e 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyMod.java @@ -13,7 +13,7 @@ * *

This is the default qualifier for unannotated types. * - * @see IteratorPolyShrink + * @see IteratorPolyMod * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownIteratorPolyShrink {} +public @interface UnknownIteratorPolyMod {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 693be47960b7..daa8ea7d74f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -13,13 +13,13 @@ import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomGrow; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrow; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.UnknownGrow; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -62,10 +62,10 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyGrow} qualifier. */ private final AnnotationMirror POLY_GROW; - /** The {@code @}{@link UnknownIteratorPolyShrink} qualifier. */ + /** The {@code @}{@link UnknownIteratorPolyMod} qualifier. */ private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPolyShrink} qualifier. */ + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -90,9 +90,9 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyShrink.class); + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyMod.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrink.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); addAliasedTypeAnnotation(Modifiable.class, GROWABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNGROWABLE); @@ -110,8 +110,8 @@ protected Set> createSupportedTypeQualifiers() { Ungrowable.class, BottomGrow.class, PolyGrow.class, - UnknownIteratorPolyShrink.class, - IteratorPolyShrink.class)); + UnknownIteratorPolyMod.class, + IteratorPolyMod.class)); } @Override @@ -167,7 +167,7 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrink}. + * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. * * @param tree the listIterator method invocation * @param methodType the annotated executable type of the invoked method @@ -196,7 +196,7 @@ private void refineListIteratorReturnType( return; } - if (hasIteratorPolyShrink(receiverType)) { + if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(GROWABLE); } else { returnType.replaceAnnotation(UNKNOWN_GROW); @@ -226,17 +226,17 @@ private boolean isListIteratorMethod( } /** - * Returns true if {@code type} has the {@code @IteratorPolyShrink} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyShrink} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation */ - private boolean hasIteratorPolyShrink(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrink.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 419844f1be7d..06eaacc363fa 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -12,12 +12,12 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplace; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplace; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownReplace; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -71,10 +71,10 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyReplace} qualifier. */ private final AnnotationMirror POLY_REPLACE; - /** The {@code @}{@link UnknownIteratorPolyShrink} qualifier. */ + /** The {@code @}{@link UnknownIteratorPolyMod} qualifier. */ private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPolyShrink} qualifier. */ + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -104,9 +104,9 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyShrink.class); + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyMod.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrink.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); addAliasedTypeAnnotation(Modifiable.class, REPLACEABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNREPLACEABLE); @@ -124,8 +124,8 @@ protected Set> createSupportedTypeQualifiers() { Unreplaceable.class, BottomReplace.class, PolyReplace.class, - UnknownIteratorPolyShrink.class, - IteratorPolyShrink.class)); + UnknownIteratorPolyMod.class, + IteratorPolyMod.class)); } @Override @@ -181,7 +181,7 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrink}. + * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. * * @param tree the listIterator method invocation * @param methodType the annotated executable type of the invoked method @@ -210,7 +210,7 @@ private void refineListIteratorReturnType( return; } - if (hasIteratorPolyShrink(receiverType)) { + if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(REPLACEABLE); } else { returnType.replaceAnnotation(UNKNOWN_REPLACE); @@ -240,17 +240,17 @@ private boolean isListIteratorMethod( } /** - * Returns true if {@code type} has the {@code @IteratorPolyShrink} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyShrink} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation */ - private boolean hasIteratorPolyShrink(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrink.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index bb3761f2b109..27240646bc48 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -12,12 +12,12 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrink; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -61,12 +61,11 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private final AnnotationMirror POLY_SHRINK; /** - * The {@code @}{@link UnknownIteratorPolyShrink} qualifier (top of iterator-preservation - * hierarchy). + * The {@code @}{@link UnknownIteratorPolyMod} qualifier (top of iterator-preservation hierarchy). */ private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPolyShrink} qualifier. */ + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -90,9 +89,9 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyShrink.class); + AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyMod.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrink.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); addAliasedTypeAnnotation(Unmodifiable.class, UNSHRINKABLE); @@ -110,8 +109,8 @@ protected Set> createSupportedTypeQualifiers() { Unshrinkable.class, BottomShrink.class, PolyShrink.class, - UnknownIteratorPolyShrink.class, - IteratorPolyShrink.class)); + UnknownIteratorPolyMod.class, + IteratorPolyMod.class)); } @Override @@ -165,10 +164,10 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code iterator()} return type based on {@code @IteratorPolyShrink}. + * Refines {@code iterator()} return type based on {@code @IteratorPolyMod}. * *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked - * method receiver type has {@code @IteratorPolyShrink}, then the result is {@code @Shrinkable + * method receiver type has {@code @IteratorPolyMod}, then the result is {@code @Shrinkable * Iterator}. Otherwise, shrinkability precision is dropped to {@code @UnknownShrink}. * * @param tree the iterator method invocation @@ -200,8 +199,8 @@ private void refineIteratorReturnType( return; } - // receiver type is @Shrinkable. check for @IteratorPolyShrink - if (hasIteratorPolyShrink(receiverType)) { + // receiver type is @Shrinkable. check for @IteratorPolyMod + if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { returnType.replaceAnnotation(UNKNOWN_SHRINK); @@ -234,17 +233,17 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT } /** - * Returns true if {@code type} has the {@code @IteratorPolyShrink} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyShrink} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation */ - private boolean hasIteratorPolyShrink(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrink.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } /** diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index c84f9e2dd4f3..72eb54f0b9e8 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -25,7 +25,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrink; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -38,7 +38,7 @@ enum TestEnum { B } - @IteratorPolyShrink List list; + @IteratorPolyMod List list; void iteratorChecker() { // this should not be allowed because CopyOnWriteArrayList's iterator does not preserve remove @@ -68,7 +68,7 @@ void copyOnWriteArrayListIterator() { List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), - // then if the receiver is @Shrinkable and has @IteratorPolyShrink, then the result is + // then if the receiver is @Shrinkable and has @IteratorPolyMod, then the result is // @Shrinkable. // otherwise, the result is unknown. // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? @@ -128,7 +128,7 @@ void KeySetIterator() { @Shrinkable Iterator iter2 = keys2.iterator(); } - void setIteratorPolyShrink() { + void setIteratorPolyMod() { Set hashSet = new HashSet<>(); @Shrinkable Iterator hashSetIterator = hashSet.iterator(); hashSetIterator.remove(); @@ -138,7 +138,7 @@ void setIteratorPolyShrink() { treeSetIterator.remove(); } - void dequeIteratorPolyShrink() { + void dequeIteratorPolyMod() { Deque arrayDeque = new ArrayDeque<>(); @Shrinkable Iterator arrayDequeIterator = arrayDeque.iterator(); arrayDequeIterator.remove(); @@ -148,7 +148,7 @@ void dequeIteratorPolyShrink() { linkedListIterator.remove(); } - void queueIteratorPolyShrink() { + void queueIteratorPolyMod() { Queue priorityQueue = new PriorityQueue<>(); @Shrinkable Iterator priorityQueueIterator = priorityQueue.iterator(); priorityQueueIterator.remove(); @@ -166,7 +166,7 @@ void queueIteratorPolyShrink() { linkedTransferQueueIterator.remove(); } - void factoryIteratorPolyShrink() { + void factoryIteratorPolyMod() { Set enumSet = EnumSet.of(TestEnum.A, TestEnum.B); @Shrinkable Iterator enumSetIterator = enumSet.iterator(); enumSetIterator.remove(); diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index f954d65ea369..515aa6287ad0 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -85,11 +85,11 @@ \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIteratorPolyShrink + @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIteratorPolyMod / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | - @BottomGrow @BottomShrink @BottomReplace @IteratorPolyShrink + @BottomGrow @BottomShrink @BottomReplace @IteratorPolyMod \end{verbatim} \end{minipage} \end{smaller} @@ -216,13 +216,13 @@ Qualifiers in the Iterator hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPolyShrink}] +\item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPolyMod}] The top qualifier. The checker cannot determine whether this collection's \code{iterator()} is \<@PolyShrink>. This is the default qualifier for unannotated types. -\item[\refqualclass{checker/modifiability/qual}{IteratorPolyShrink}] +\item[\refqualclass{checker/modifiability/qual}{IteratorPolyMod}] This collection's \code{iterator()} is \<@PolyShrink>. That is, if collection \ is \<@Shrinkable>, then \ is also \<@Shrinkable>. @@ -358,13 +358,13 @@ \end{Verbatim} A collection uses the second signature if the collection is -\<@IteratorPolyShrink>. The collection uses the first signature if the -collection is \<@UnknownIteratorPolyShrink>. +\<@IteratorPolyMod>. The collection uses the first signature if the +collection is \<@UnknownIteratorPolyMod>. Here are examples of its use: \begin{Verbatim} - @IteratorPolyShrink @Shrinkable List list = ...; + @IteratorPolyMod @Shrinkable List list = ...; @Shrinkable Iterator it = list.iterator(); it.remove(); // OK @@ -444,4 +444,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace -% LocalWords: UnknownIteratorPolyShrink IteratorPolyShrink +% LocalWords: UnknownIteratorPolyMod IteratorPolyMod From 36fc863b948cc6ba3bb30201d0819ca8ffcd9a63 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 4 May 2026 15:57:42 -0700 Subject: [PATCH 154/308] add more cases to SubtypeCapabilityPreservationTest Co-authored-by: Copilot --- .../SubtypeCapabilityPreservationTest.java | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java index 59de0a98ce14..39944f095c64 100644 --- a/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java +++ b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java @@ -1,39 +1,49 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.ListIterator; import java.util.Queue; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; // Tests that storing a modifiable subtype in a less-capable supertype does not permanently discard // modifiability dimensions that the subtype supports. class SubtypeCapabilityPreservationTest { void listIteratorThroughIterator() { - @Modifiable ListIterator listIterator = new LinkedList().listIterator(); - @Modifiable Iterator iterator = listIterator; - @Modifiable ListIterator roundTrip = (ListIterator) iterator; - roundTrip.add("added"); - roundTrip.set("replaced"); - roundTrip.remove(); + List list = new ArrayList<>(); + @Modifiable ListIterator listIterator = list.listIterator(); + @Shrinkable Iterator iterator = listIterator; + @Modifiable ListIterator listIterator2 = (ListIterator) iterator; + listIterator2.add("added"); + listIterator2.set("replaced"); + listIterator2.remove(); + + List unmodList = Collections.unmodifiableList(new ArrayList<>()); + @Unmodifiable ListIterator listIterator3 = unmodList.listIterator(); + Iterator iterator2 = listIterator3; + @Modifiable ListIterator listIterator4 = (ListIterator) iterator2; } void linkedListThroughQueue() { @Modifiable LinkedList linkedList = new LinkedList<>(); @Modifiable Queue queue = linkedList; - @Modifiable LinkedList roundTrip = (LinkedList) queue; - roundTrip.add("added"); - roundTrip.set(0, "replaced"); - roundTrip.remove(); + @Modifiable LinkedList linkedList2 = (LinkedList) queue; + linkedList2.add("added"); + linkedList2.set(0, "replaced"); + linkedList2.remove(); } void arrayListThroughCollection() { - @Modifiable ArrayList arrayList = new ArrayList<>(); + @Modifiable List arrayList = new ArrayList<>(); @Modifiable Collection collection = arrayList; - @Modifiable ArrayList roundTrip = (ArrayList) collection; - roundTrip.add("added"); - roundTrip.set(0, "replaced"); - roundTrip.remove(0); + @Modifiable List arrayList2 = (ArrayList) collection; + arrayList2.add("added"); + arrayList2.set(0, "replaced"); + arrayList2.remove(0); } } From 7c14cd2a55d72df3a283eed0b759d663080e69ad Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 4 May 2026 17:44:05 -0700 Subject: [PATCH 155/308] Better way to test --- .../checkerframework/framework/util/ContractsFromMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 483dce6a7078..64ec39101d5d 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -126,7 +126,7 @@ public Set getConditionalPostconditions( private Set getContractsOfKind( ExecutableElement executableElement, Contract.Kind kind, Class clazz) { MethodTree methodDecl; - if (executableElement.getSimpleName().toString().equals("")) { + if (executableElement.getKind() == ElementKind.CONSTRUCTOR) { // It's a constructor, not a method. methodDecl = null; } else { From de201009e69eff1d94ca5971a928c28e9041fcc7 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 4 May 2026 17:51:07 -0700 Subject: [PATCH 156/308] Add import --- .../org/checkerframework/framework/util/ContractsFromMethod.java | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 64ec39101d5d..73ef62380e3b 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -9,6 +9,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.type.TypeMirror; From f688f0817cfa28e0284173fda2a17c1f1aaf79da Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 4 May 2026 18:02:00 -0700 Subject: [PATCH 157/308] Remove nullness annotation --- .../checkerframework/framework/util/ContractsFromMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 73ef62380e3b..422208f9e123 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -296,7 +296,7 @@ private Set getContract( private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( AnnotationMirror contractAnno, @Nullable AnnotationMirror argumentAnno, - @Nullable Map argumentRenaming, + Map argumentRenaming, @Nullable TypeMirror type) { @SuppressWarnings("deprecation") // permitted for use in the framework From 56903e86434bf294068eb60be459d70842fedb86 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 5 May 2026 15:50:46 -0700 Subject: [PATCH 158/308] fixed javadoc in modifiable and unmodifiable --- .../checkerframework/checker/modifiability/qual/Modifiable.java | 2 +- .../checker/modifiability/qual/Unmodifiable.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index dd916fdfd088..861c92453b59 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -13,7 +13,7 @@ * *

As an exception, {@code @Modifiable} means {@code @Unknown*} if the type does not support a * given category of operation; for example, {@code @Modifiable Iterator} means - * {@code @UnknownGrow @UnknownShrink @Replaceable Iterator}. + * {@code @UnknownGrow @Shrinkable @UnknownReplace Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 870ad74afa29..37618672a08a 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -13,7 +13,7 @@ * *

As an exception, {@code @Unmodifiable} means {@code @Unknown*} if the type does not support a * given category of operation; for example, {@code @Unmodifiable Iterator} means - * {@code @UnknownGrow @UnknownShrink @Unreplaceable Iterator}. + * {@code @UnknownGrow @Unshrinkable @UnknownReplace Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ From 0d733aed6df22d2705716b17340facc45d5b36bf Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 5 May 2026 16:31:58 -0700 Subject: [PATCH 159/308] refine SubtypeCapabilityPreservationTest --- .../modifiability/SubtypeCapabilityPreservationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java index 39944f095c64..8534033213af 100644 --- a/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java +++ b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java @@ -7,7 +7,6 @@ import java.util.ListIterator; import java.util.Queue; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; // Tests that storing a modifiable subtype in a less-capable supertype does not permanently discard @@ -17,7 +16,7 @@ class SubtypeCapabilityPreservationTest { void listIteratorThroughIterator() { List list = new ArrayList<>(); @Modifiable ListIterator listIterator = list.listIterator(); - @Shrinkable Iterator iterator = listIterator; + Iterator iterator = listIterator; @Modifiable ListIterator listIterator2 = (ListIterator) iterator; listIterator2.add("added"); listIterator2.set("replaced"); @@ -26,6 +25,7 @@ void listIteratorThroughIterator() { List unmodList = Collections.unmodifiableList(new ArrayList<>()); @Unmodifiable ListIterator listIterator3 = unmodList.listIterator(); Iterator iterator2 = listIterator3; + // :: error: [assignment] @Modifiable ListIterator listIterator4 = (ListIterator) iterator2; } From 6d5d7871d33b960d419c42b75428f8f01fb6792f Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 6 May 2026 11:04:12 -0700 Subject: [PATCH 160/308] add test for canonical annotation Co-authored-by: Copilot --- .../AliasCanonicalizationTest.java | 75 +++++++++++++++++++ checker/tests/modifiability/SetTest.java | 72 +++++++++--------- 2 files changed, 112 insertions(+), 35 deletions(-) create mode 100644 checker/tests/modifiability/AliasCanonicalizationTest.java diff --git a/checker/tests/modifiability/AliasCanonicalizationTest.java b/checker/tests/modifiability/AliasCanonicalizationTest.java new file mode 100644 index 000000000000..97e4803a18b2 --- /dev/null +++ b/checker/tests/modifiability/AliasCanonicalizationTest.java @@ -0,0 +1,75 @@ +import java.util.Collection; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnknownGrow; +import org.checkerframework.checker.modifiability.qual.UnknownReplace; +import org.checkerframework.checker.modifiability.qual.UnknownShrink; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; + +class AliasCanonicalizationTest { + + void modifiableIterator(@Modifiable Iterator iterator) { + @UnknownGrow @Shrinkable @UnknownReplace Iterator canonical = iterator; + @Shrinkable Iterator shrinkable = iterator; + iterator.remove(); + } + + void unmodifiableIterator(@Unmodifiable Iterator iterator) { + @UnknownGrow @Unshrinkable @UnknownReplace Iterator canonical = iterator; + // :: error: [method.invocation] + iterator.remove(); + } + + void modifiableSet(@Modifiable Set set) { + @Growable @Shrinkable @UnknownReplace Set canonical = set; + set.add("a"); + set.remove("a"); + } + + void modifiableQueue(@Modifiable Queue queue) { + @Growable @Shrinkable @UnknownReplace Queue canonical = queue; + queue.add("a"); + queue.remove(); + } + + void modifiableCollection(@Modifiable Collection collection) { + @Growable @Shrinkable @UnknownReplace Collection canonical = collection; + collection.add("a"); + collection.remove("a"); + } + + void modifiableEntry(Map.@Modifiable Entry entry) { + Map.@UnknownGrow @UnknownShrink @Replaceable Entry canonical = entry; + entry.setValue("value"); + } + + void explicitIterator(@Growable @Shrinkable @Replaceable Iterator iterator) { + @Growable @Shrinkable @Replaceable Iterator explicit = iterator; + @Replaceable Iterator replaceable = iterator; + @Growable Iterator growable = iterator; + } + + void explicitReplaceOnStructurallyUnreplaceableTypes( + @Replaceable Collection collection, + @Replaceable Set set, + @Replaceable Queue queue) { + @Replaceable Collection explicitCollection = collection; + @Replaceable Set explicitSet = set; + @Replaceable Queue explicitQueue = queue; + } + + void modifiableListIterator(@Modifiable ListIterator iterator) { + @Growable @Shrinkable @Replaceable ListIterator canonical = iterator; + iterator.add("a"); + iterator.set("b"); + iterator.remove(); + } +} diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index f0781670f82f..71816b7cef3b 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -13,37 +13,29 @@ /** * Tests structural capability removal for Set, Queue, Map.Entry, and Iterator. * - *

Set / Queue: TypeAnnotator removes the Replace capability (sets Replace to @UnknownReplace). - * Map.Entry: TypeAnnotator removes Grow and Shrink capabilities. Iterator: TypeAnnotator removes - * Grow and Replace capabilities. + *

Convenience aliases on Set / Queue lose the Replace capability (set Replace + * to @UnknownReplace). Convenience aliases on Map.Entry lose Grow and Shrink capabilities. + * Convenience aliases on Iterator lose Grow and Replace capabilities. Explicit capability + * annotations are preserved. */ class SetTest { // ========================================================== - // Set: Replace capability is always removed by the TypeAnnotator. - // Effective types have Replace = @UnknownReplace. + // Set: explicit Replace capability is preserved. // ========================================================== void testSetReplaceRemoved( @Replaceable Set replaceable, @Growable @Replaceable Set growReplace, @Shrinkable @Replaceable Set shrinkReplace, - @Growable @Shrinkable @Replaceable Set modifiable) { + @Modifiable Set modifiable) { - // After TypeAnnotator, all Set Replace qualifiers become @UnknownReplace. - // So @Replaceable Set, @Growable @Replaceable Set, etc. lose their Replaceable. - - // @Replaceable Set → Grow=Unknown, Shrink=Unknown, Replace=Unknown (R removed) - // @Growable @Replaceable Set → Grow=Growable, Shrink=Unknown, Replace=Unknown (R removed) @Growable Set g1 = growReplace; // OK: Grow still intact - @Replaceable Set r1 = - growReplace; // OK: @replaceable set becomes @UnknownMod, GrowReplace set becomes @Growable. + @Replaceable Set r1 = growReplace; - // @Growable @Shrinkable @Replaceable Set → Grow=G, Shrink=S, Replace=Unknown (R removed) - // effectively the same as @Growable @Shrinkable Set @Growable @Shrinkable Set gs1 = modifiable; // OK - @Replaceable Set r2 = - modifiable; // OK: replaceale becomes unknown, modifiable becomes @Growable @Shrinkable + // :: error: [assignment] + @Replaceable Set r2 = modifiable; //unknownReplace cannot assign to replaceable. } void testSetGrowShrinkPreserved( @@ -69,10 +61,13 @@ void testSetGrowShrinkPreserved( } void testQueueLikeSet( - @Modifiable Queue modifiable, @Growable @Shrinkable Queue gs) { - // Queue also has Replace removed (same rule as Set). - @Growable @Shrinkable Queue q1 = modifiable; // OK: modifiable <: gs (R removed from both) - @Growable @Shrinkable @Replaceable Queue q2 = gs; // OK: same after TypeAnnotator + @Modifiable Queue modifiable, + @Growable @Shrinkable @Replaceable Queue explicit) { + // @Modifiable Queue loses only its Replace capability. + @Growable @Shrinkable Queue q1 = modifiable; + // :: error: [assignment] + @Growable @Shrinkable @Replaceable Queue q2 = modifiable; + @Growable @Shrinkable @Replaceable Queue q3 = explicit; } // ========================================================== @@ -90,30 +85,34 @@ void testEntryAssignment( Map.@Replaceable Entry replaceable, Map.@Growable @Replaceable Entry growReplace, Map.@Shrinkable @Replaceable Entry shrinkReplace, - Map.@Growable @Shrinkable @Replaceable Entry modifiable) { + Map.@Modifiable Entry modifiable, + Map.@Growable @Shrinkable @Replaceable Entry explicit) { // Group 1: entries without Replace bit become @UnknownGrow @UnknownShrink @UnknownReplace // (G and S are removed → both become Unknown; R was already Unknown) Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u1 = growable; Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u2 = shrinkable; Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u3 = gs; - // After TypeAnnotator they are all effectively @UnknownGrow @UnknownShrink @UnknownReplace, - // so assignments between them are valid. - Map.@Growable Entry g1 = unknown; // OK: same effective type + // Explicit Grow/Shrink annotations are preserved. + // :: error: [assignment] + Map.@Growable Entry g1 = unknown; + // :: error: [assignment] Map.@Shrinkable Entry s1 = unknown; Map.@Growable Entry g2 = gs; // OK - // Group 2: entries with Replace bit become @UnknownGrow @UnknownShrink @Replaceable - // (G and S are removed; R=Replaceable is preserved) Map.@UnknownGrow @UnknownShrink @Replaceable Entry r1 = growReplace; Map.@UnknownGrow @UnknownShrink @Replaceable Entry r2 = shrinkReplace; Map.@UnknownGrow @UnknownShrink @Replaceable Entry r3 = modifiable; - // After TypeAnnotator they are all effectively @UnknownGrow @UnknownShrink @Replaceable. + // Explicit Grow/Shrink annotations are preserved on Map.Entry. + // :: error: [assignment] Map.@Growable @Replaceable Entry gr1 = replaceable; // OK: same effective type + // :: error: [assignment] Map.@Shrinkable @Replaceable Entry sr1 = replaceable; - Map.@Growable @Shrinkable @Replaceable Entry m1 = replaceable; + Map.@Modifiable Entry m1 = replaceable; + // :: error: [assignment] Map.@Shrinkable @Replaceable Entry sr2 = growReplace; - Map.@Growable @Replaceable Entry gr2 = modifiable; + Map.@Replaceable Entry gr2 = modifiable; + Map.@Modifiable Entry m2 = explicit; // Cross-group: Replace !<: UnknownReplace (wrong direction) is NOT an error — // but UnknownReplace !<: Replaceable IS an error: @@ -136,15 +135,18 @@ void testIteratorAssignment( @Shrinkable Iterator shrinkable, @Growable @Shrinkable Iterator growShrink, @Shrinkable @Replaceable Iterator shrinkReplace, - @Growable @Shrinkable @Replaceable Iterator modifiable) { + @Modifiable Iterator modifiable, + @Growable @Shrinkable @Replaceable Iterator explicit) { // Group 1: No Shrink bit → effective @UnknownGrow @UnknownShrink @UnknownReplace @UnknownGrow @UnknownShrink @UnknownReplace Iterator u1 = growable; @UnknownGrow @UnknownShrink @UnknownReplace Iterator u2 = replaceable; @UnknownGrow @UnknownShrink @UnknownReplace Iterator u3 = growReplace; - // They are all effectively the same type after TypeAnnotator: + // Explicit Grow/Replace annotations are preserved. + // :: error: [assignment] @Growable Iterator g1 = unknown; @Replaceable Iterator r1 = growReplace; + // :: error: [assignment] @Growable @Replaceable Iterator gr1 = replaceable; // Group 2: Has Shrink bit → effective @UnknownGrow @Shrinkable @UnknownReplace @@ -154,11 +156,11 @@ void testIteratorAssignment( @UnknownGrow @Shrinkable @UnknownReplace Iterator s4 = modifiable; // Cross-assignments within group 2: @Shrinkable Iterator sh1 = modifiable; + // :: error: [assignment] @Growable @Shrinkable Iterator gs1 = modifiable; + // :: error: [assignment] @Shrinkable @Replaceable Iterator sr1 = modifiable; - @Growable @Shrinkable Iterator gs2 = shrinkable; - @Shrinkable @Replaceable Iterator sr2 = growShrink; - + @Modifiable Iterator m1 = explicit; // :: error: [assignment] @Shrinkable Iterator bad1 = growable; // Error: Shrink=Unknown !<: Shrinkable } From 0be4792bd76743fb74eae0898275da751ce0dda3 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 6 May 2026 12:25:41 -0700 Subject: [PATCH 161/308] finished overriding canonical annotation --- .../grow/GrowAnnotatedTypeFactory.java | 118 ++++++--------- .../replace/ReplaceAnnotatedTypeFactory.java | 138 +++++------------- .../shrink/ShrinkAnnotatedTypeFactory.java | 106 +++++--------- .../IteratorConflictingAnnosUse.java | 4 - checker/tests/modifiability/SetTest.java | 4 +- 5 files changed, 120 insertions(+), 250 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index daa8ea7d74f9..10de83f1aad9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -8,6 +8,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; @@ -22,16 +23,12 @@ import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.QualifierUpperBounds; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -62,9 +59,6 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyGrow} qualifier. */ private final AnnotationMirror POLY_GROW; - /** The {@code @}{@link UnknownIteratorPolyMod} qualifier. */ - private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; @@ -89,15 +83,9 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); - this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyMod.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); - addAliasedTypeAnnotation(Modifiable.class, GROWABLE); - addAliasedTypeAnnotation(Unmodifiable.class, UNGROWABLE); - addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_GROW); - addAliasedTypeAnnotation(PolyModifiable.class, POLY_GROW); postInit(); } @@ -114,9 +102,33 @@ protected Set> createSupportedTypeQualifiers() { IteratorPolyMod.class)); } + /** + * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for + * aliases whose meaning depends on the annotated type. + * + *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types + * that cannot grow structurally, such as {@code Map.Entry} and non-{@code ListIterator} {@code + * Iterator}, their grow component canonicalizes to {@code @UnknownGrow}. Explicit grow qualifiers + * are left to normal canonicalization so users can still write and preserve an explicit + * capability tuple such as {@code @Growable @Shrinkable @Replaceable Iterator}. + * + *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrow}, but for + * {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its grow + * bit is therefore {@code @UnknownGrow}. + */ @Override - protected TypeAnnotator createTypeAnnotator() { - return new ListTypeAnnotator(new GrowTypeAnnotator(this), super.createTypeAnnotator()); + public AnnotationMirror canonicalAnnotation( + AnnotationMirror annotation, @Nullable TypeMirror tm) { + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeCannotGrow(tm) ? UNKNOWN_GROW : GROWABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeCannotGrow(tm) ? UNKNOWN_GROW : UNGROWABLE; + } else if (areSameByClass(annotation, UnknownModifiability.class)) { + return UNKNOWN_GROW; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return tm != null && isMapEntry(tm) ? UNKNOWN_GROW : POLY_GROW; + } + return super.canonicalAnnotation(annotation); } @Override @@ -239,69 +251,21 @@ private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } - /** - * Removes grow capability for types that structurally cannot support it: - * - *

    - *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to - * {@code @UnknownReplace} - *
  • Map.Entry: remove Grow and Shrink capabilities - *
  • Iterator: remove Grow and Replace capabilities - *
- */ - private class GrowTypeAnnotator extends TypeAnnotator { - /** - * Creates a new GrowTypeAnnotator. - * - * @param factory the associated type factory - */ - public GrowTypeAnnotator(GrowAnnotatedTypeFactory factory) { - super(factory); - } - - @Override - public Void visitDeclared(AnnotatedDeclaredType type, Void p) { - super.visitDeclared(type, p); - - // Skip structural refinement for polymorphic types. - if (type.hasPrimaryAnnotation(POLY_GROW)) { - return null; - } - - TypeMirror underlyingType = type.getUnderlyingType(); - - if (TypesUtils.isErasedSubtype(underlyingType, mapEntryErasure, types)) { - // Map.Entry: no grow. - type.replaceAnnotation(UNKNOWN_GROW); - } else if (TypesUtils.isErasedSubtype(underlyingType, iteratorErasure, types) - && !TypesUtils.isErasedSubtype(underlyingType, listIteratorErasure, types)) { - // Iterator: no grow. - type.replaceAnnotation(UNKNOWN_GROW); - } - - return null; + /** Returns true if {@code type} structurally cannot support grow operations. */ + private boolean typeCannotGrow(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; } + return isMapEntry(type) + || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } - @Override - protected QualifierUpperBounds createQualifierUpperBounds() { - return new QualifierUpperBounds(this) { - private final AnnotationMirrorSet unknownGrowAndIter = - AnnotationMirrorSet.unmodifiableSet(Arrays.asList(UNKNOWN_GROW, UNKNOWN_ITER)); - - @Override - public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { - if (TypesUtils.isErasedSubtype(type, mapEntryErasure, types)) { - // Map.Entry cannot be grown. - } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) - && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { - // Standard Iterator (not ListIterator) cannot be grown. - return unknownGrowAndIter; - } - AnnotationMirrorSet bounds = new AnnotationMirrorSet(super.getBoundQualifiers(type)); - bounds.add(UNKNOWN_ITER); - return bounds; - } - }; + /** Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. */ + private boolean isMapEntry(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 06eaacc363fa..57cd9df930c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -8,6 +8,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; @@ -22,16 +23,12 @@ import org.checkerframework.checker.modifiability.qual.UnknownReplace; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unreplaceable; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.QualifierUpperBounds; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -71,9 +68,6 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyReplace} qualifier. */ private final AnnotationMirror POLY_REPLACE; - /** The {@code @}{@link UnknownIteratorPolyMod} qualifier. */ - private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; @@ -103,15 +97,9 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); - this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyMod.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); - addAliasedTypeAnnotation(Modifiable.class, REPLACEABLE); - addAliasedTypeAnnotation(Unmodifiable.class, UNREPLACEABLE); - addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_REPLACE); - addAliasedTypeAnnotation(PolyModifiable.class, POLY_REPLACE); postInit(); } @@ -128,9 +116,34 @@ protected Set> createSupportedTypeQualifiers() { IteratorPolyMod.class)); } + /** + * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for + * aliases whose meaning depends on the annotated type. + * + *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types + * that cannot replace structurally, such as exact {@code Collection}, {@code Set}, non-{@code + * LinkedList} {@code Queue}, and non-{@code ListIterator} {@code Iterator}, their replace + * component canonicalizes to {@code @UnknownReplace}. Explicit replace qualifiers are left to + * normal canonicalization so users can still write and preserve an explicit capability tuple such + * as {@code @Growable @Shrinkable @Replaceable Set}. + * + *

{@code @PolyModifiable} remains {@code @PolyReplace}: unlike grow and shrink for {@code + * Map.Entry}, replacement through {@code Entry.setValue} is a meaningful capability to carry from + * the map receiver. + */ @Override - protected TypeAnnotator createTypeAnnotator() { - return new ListTypeAnnotator(new ReplaceTypeAnnotator(this), super.createTypeAnnotator()); + public AnnotationMirror canonicalAnnotation( + AnnotationMirror annotation, @Nullable TypeMirror tm) { + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeCannotReplace(tm) ? UNKNOWN_REPLACE : REPLACEABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeCannotReplace(tm) ? UNKNOWN_REPLACE : UNREPLACEABLE; + } else if (areSameByClass(annotation, UnknownModifiability.class)) { + return UNKNOWN_REPLACE; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return POLY_REPLACE; + } + return super.canonicalAnnotation(annotation); } @Override @@ -253,88 +266,17 @@ private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } - /** - * Removes capabilities that cannot be supported by structural constraints of the collection type: - * - *

    - *
  • Collection itself (not subtypes): remove Replace capability → set Replace to - * {@code @UnknownReplace} - *
  • Set or Queue (not LinkedList): remove Replace capability → set Replace to - * {@code @UnknownReplace} - *
  • Iterator: remove Replace capabilities - *
- */ - private class ReplaceTypeAnnotator extends TypeAnnotator { - /** - * Creates a new ReplaceTypeAnnotator. - * - * @param factory the associated type factory - */ - public ReplaceTypeAnnotator(ReplaceAnnotatedTypeFactory factory) { - super(factory); - } - - @Override - public Void visitDeclared(AnnotatedDeclaredType type, Void p) { - super.visitDeclared(type, p); - - // Skip structural refinement for polymorphic types. - if (type.hasPrimaryAnnotation(POLY_REPLACE)) { - return null; - } - - TypeMirror underlyingType = type.getUnderlyingType(); - TypeMirror erasedUnderlyingType = types.erasure(underlyingType); - - if (types.isSameType(erasedUnderlyingType, collectionErasure)) { - // Collection itself: Drop R bit - type.replaceAnnotation(UNKNOWN_REPLACE); - } else if (TypesUtils.isErasedSubtype(underlyingType, setErasure, types)) { - // Set: Drop R bit - type.replaceAnnotation(UNKNOWN_REPLACE); - } else if (TypesUtils.isErasedSubtype(underlyingType, queueErasure, types) - && !TypesUtils.isErasedSubtype(underlyingType, linkedListErasure, types)) { - // Queue (but not LinkedList): Drop R bit - type.replaceAnnotation(UNKNOWN_REPLACE); - } else if (TypesUtils.isErasedSubtype(underlyingType, iteratorErasure, types) - && !TypesUtils.isErasedSubtype(underlyingType, listIteratorErasure, types)) { - // Iterator: Drop R bits - type.replaceAnnotation(UNKNOWN_REPLACE); - } - return null; + /** Returns true if {@code type} structurally cannot support replace operations. */ + private boolean typeCannotReplace(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; } - } - - @Override - protected QualifierUpperBounds createQualifierUpperBounds() { - return new QualifierUpperBounds(this) { - private final AnnotationMirrorSet unknownReplaceAndIter = - AnnotationMirrorSet.unmodifiableSet(Arrays.asList(UNKNOWN_REPLACE, UNKNOWN_ITER)); - - @Override - public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { - TypeMirror erasedType = types.erasure(type); - if (types.isSameType(erasedType, collectionErasure)) { - // Elements of a raw Collection are treated as @UnknownReplace. - return unknownReplaceAndIter; - } else if (TypesUtils.isErasedSubtype(type, setErasure, types)) { - // Elements of a set can never be replaced, so treat them as @UnknownReplace. Even if - // they are annotation @Modifiable in a stubfile. - return unknownReplaceAndIter; - } else if (TypesUtils.isErasedSubtype(type, queueErasure, types) - && !TypesUtils.isErasedSubtype(type, linkedListErasure, types)) { - // Elements of a queue (but not LinkedList) can never be replaced, so treat them as - // @UnknownReplace. - return unknownReplaceAndIter; - } else if (TypesUtils.isErasedSubtype(type, iteratorErasure, types) - && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)) { - // Iterators cannot replace elements. - return unknownReplaceAndIter; - } - AnnotationMirrorSet bounds = new AnnotationMirrorSet(super.getBoundQualifiers(type)); - bounds.add(UNKNOWN_ITER); - return bounds; - } - }; + TypeMirror erasedType = types.erasure(type); + return types.isSameType(erasedType, collectionErasure) + || TypesUtils.isErasedSubtype(type, setErasure, types) + || (TypesUtils.isErasedSubtype(type, queueErasure, types) + && !TypesUtils.isErasedSubtype(type, linkedListErasure, types)) + || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 27240646bc48..7342a914b4a0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -8,6 +8,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; @@ -22,17 +23,13 @@ import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.QualifierUpperBounds; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -60,11 +57,6 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyShrink} qualifier. */ private final AnnotationMirror POLY_SHRINK; - /** - * The {@code @}{@link UnknownIteratorPolyMod} qualifier (top of iterator-preservation hierarchy). - */ - private final AnnotationMirror UNKNOWN_ITER; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; @@ -88,15 +80,9 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); - this.UNKNOWN_ITER = - AnnotationBuilder.fromClass(getElementUtils(), UnknownIteratorPolyMod.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); - addAliasedTypeAnnotation(Modifiable.class, SHRINKABLE); - addAliasedTypeAnnotation(Unmodifiable.class, UNSHRINKABLE); - addAliasedTypeAnnotation(UnknownModifiability.class, UNKNOWN_SHRINK); - addAliasedTypeAnnotation(PolyModifiable.class, POLY_SHRINK); postInit(); } @@ -113,9 +99,33 @@ protected Set> createSupportedTypeQualifiers() { IteratorPolyMod.class)); } + /** + * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for + * aliases whose meaning depends on the annotated type. + * + *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on {@code + * Map.Entry}, which cannot shrink, their shrink component canonicalizes to + * {@code @UnknownShrink}. Explicit shrink qualifiers are left to normal canonicalization so users + * can still write and preserve an explicit capability tuple such as + * {@code @Growable @Shrinkable @Replaceable Entry}. + * + *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrink}, but for + * {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its shrink + * bit is therefore {@code @UnknownShrink}. + */ @Override - protected TypeAnnotator createTypeAnnotator() { - return new ListTypeAnnotator(new ShrinkTypeAnnotator(this), super.createTypeAnnotator()); + public AnnotationMirror canonicalAnnotation( + AnnotationMirror annotation, @Nullable TypeMirror tm) { + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeCannotShrink(tm) ? UNKNOWN_SHRINK : SHRINKABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeCannotShrink(tm) ? UNKNOWN_SHRINK : UNSHRINKABLE; + } else if (areSameByClass(annotation, UnknownModifiability.class)) { + return UNKNOWN_SHRINK; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return tm != null && isMapEntry(tm) ? UNKNOWN_SHRINK : POLY_SHRINK; + } + return super.canonicalAnnotation(annotation); } @Override @@ -246,58 +256,16 @@ private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } - /** - * Removes shrink capability for types that structurally cannot support it: - * - *

    - *
  • Map.Entry: cannot shrink, set to {@code @UnknownShrink} - *
- */ - private class ShrinkTypeAnnotator extends TypeAnnotator { - - /** - * Creates a new ShrinkTypeAnnotator. - * - * @param factory the associated type factory - */ - public ShrinkTypeAnnotator(ShrinkAnnotatedTypeFactory factory) { - super(factory); - } - - @Override - public Void visitDeclared(AnnotatedDeclaredType type, Void p) { - super.visitDeclared(type, p); - - // Skip structural refinement for polymorphic types. - if (type.hasPrimaryAnnotation(POLY_SHRINK)) { - return null; - } - - TypeMirror underlyingType = type.getUnderlyingType(); - - if (TypesUtils.isErasedSubtype(underlyingType, mapEntryErasure, types)) { - // Map.Entry: no shrink. - type.replaceAnnotation(UNKNOWN_SHRINK); - } - - return null; - } + /** Returns true if {@code type} structurally cannot support shrink operations. */ + private boolean typeCannotShrink(TypeMirror type) { + return isMapEntry(type); } - @Override - protected QualifierUpperBounds createQualifierUpperBounds() { - return new QualifierUpperBounds(this) { - @Override - public AnnotationMirrorSet getBoundQualifiers(TypeMirror type) { - if (TypesUtils.isErasedSubtype(type, mapEntryErasure, types)) { - // Map.Entry uses fixed upper bounds in both supported hierarchies. - AnnotationMirrorSet bounds = new AnnotationMirrorSet(); - bounds.add(UNKNOWN_SHRINK); - bounds.add(UNKNOWN_ITER); - return bounds; - } - return super.getBoundQualifiers(type); - } - }; + /** Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. */ + private boolean isMapEntry(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } } diff --git a/checker/tests/modifiability/IteratorConflictingAnnosUse.java b/checker/tests/modifiability/IteratorConflictingAnnosUse.java index e7ba6370de89..dad83b143ea3 100644 --- a/checker/tests/modifiability/IteratorConflictingAnnosUse.java +++ b/checker/tests/modifiability/IteratorConflictingAnnosUse.java @@ -1,15 +1,11 @@ import java.util.Iterator; import org.checkerframework.checker.modifiability.qual.Unmodifiable; -// line 12 is throwing an error -// IteratorConflictingAnnosUse.java:12: error: [conflicting.annos] -// when running modifiabilityTest, but this error is not thrown when running this file alone. public class IteratorConflictingAnnosUse { private final IteratorConflictingAnnosTypeTuple inputTypes = new IteratorConflictingAnnosTypeTuple(); @Unmodifiable Iterator reproduce() { - // :: error: [conflicting.annos] return inputTypes.iterator(); } } diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 71816b7cef3b..3485081cb076 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -35,7 +35,7 @@ void testSetReplaceRemoved( @Growable @Shrinkable Set gs1 = modifiable; // OK // :: error: [assignment] - @Replaceable Set r2 = modifiable; //unknownReplace cannot assign to replaceable. + @Replaceable Set r2 = modifiable; // unknownReplace cannot assign to replaceable. } void testSetGrowShrinkPreserved( @@ -95,7 +95,7 @@ void testEntryAssignment( Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u3 = gs; // Explicit Grow/Shrink annotations are preserved. // :: error: [assignment] - Map.@Growable Entry g1 = unknown; + Map.@Growable Entry g1 = unknown; // :: error: [assignment] Map.@Shrinkable Entry s1 = unknown; Map.@Growable Entry g2 = gs; // OK From 4897c61d4c7a579317958735f45ac8d5d3f64702 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 6 May 2026 13:43:09 -0700 Subject: [PATCH 162/308] add documentation --- .../grow/GrowAnnotatedTypeFactory.java | 14 ++++++++++++-- .../replace/ReplaceAnnotatedTypeFactory.java | 7 ++++++- .../shrink/ShrinkAnnotatedTypeFactory.java | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 10de83f1aad9..644eac8504e6 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -251,7 +251,12 @@ private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } - /** Returns true if {@code type} structurally cannot support grow operations. */ + /** + * Returns true if {@code type} structurally cannot support grow operations. + * + * @param type the type to test + * @return true if {@code type} structurally cannot support grow operations + */ private boolean typeCannotGrow(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { return false; @@ -261,7 +266,12 @@ private boolean typeCannotGrow(TypeMirror type) { && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } - /** Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. */ + /** + * Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. + * + * @param type the type to test + * @return true if {@code type} is a subtype of {@link java.util.Map.Entry} + */ private boolean isMapEntry(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { return false; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 57cd9df930c4..5f67f72d8f09 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -266,7 +266,12 @@ private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } - /** Returns true if {@code type} structurally cannot support replace operations. */ + /** + * Returns true if {@code type} structurally cannot support replace operations. + * + * @param type the type to test + * @return true if {@code type} structurally cannot support replace operations + */ private boolean typeCannotReplace(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { return false; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 7342a914b4a0..cb20fbe681ef 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -256,12 +256,22 @@ private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } - /** Returns true if {@code type} structurally cannot support shrink operations. */ + /** + * Returns true if {@code type} structurally cannot support shrink operations. + * + * @param type the type to test + * @return true if {@code type} structurally cannot support shrink operations + */ private boolean typeCannotShrink(TypeMirror type) { return isMapEntry(type); } - /** Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. */ + /** + * Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. + * + * @param type the type to test + * @return true if {@code type} is a subtype of {@link java.util.Map.Entry} + */ private boolean isMapEntry(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { return false; From 20567a7218677d1bb1e6d751ec0ed9642dd8dde8 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 7 May 2026 12:01:25 -0700 Subject: [PATCH 163/308] add below java21 jdk skip test in ThrowsUOEtest.java --- checker/tests/modifiability/ThrowsUOETest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker/tests/modifiability/ThrowsUOETest.java b/checker/tests/modifiability/ThrowsUOETest.java index 7d0808667a21..78869ba5a657 100644 --- a/checker/tests/modifiability/ThrowsUOETest.java +++ b/checker/tests/modifiability/ThrowsUOETest.java @@ -1,3 +1,5 @@ +// @below-java21-jdk-skip-test + import java.util.NavigableMap; import java.util.SortedMap; import java.util.SortedSet; From fcb858238bfef90bff8c441bd6442c0652379df6 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 May 2026 17:28:53 -0700 Subject: [PATCH 164/308] Undo changes in `ContractsFromMethod` --- .../framework/util/ContractsFromMethod.java | 99 ++++++------------- 1 file changed, 31 insertions(+), 68 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java index 422208f9e123..16cb11061b7f 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsFromMethod.java @@ -1,6 +1,5 @@ package org.checkerframework.framework.util; -import com.sun.source.tree.MethodTree; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -9,14 +8,10 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; -import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.expression.JavaExpression; -import org.checkerframework.dataflow.expression.JavaExpressionParseException; import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; import org.checkerframework.framework.qual.EnsuresQualifier; import org.checkerframework.framework.qual.EnsuresQualifierIf; @@ -126,21 +121,13 @@ public Set getConditionalPostconditions( */ private Set getContractsOfKind( ExecutableElement executableElement, Contract.Kind kind, Class clazz) { - MethodTree methodDecl; - if (executableElement.getKind() == ElementKind.CONSTRUCTOR) { - // It's a constructor, not a method. - methodDecl = null; - } else { - methodDecl = (MethodTree) factory.declarationFromElement(executableElement); - } - Set result = new LinkedHashSet<>(); // Check for a single framework-defined contract annotation. // The result is RequiresQualifier, EnsuresQualifier, EnsuresQualifierIf, or null. AnnotationMirror frameworkContractAnno = factory.getDeclAnnotation(executableElement, kind.frameworkContractClass); if (frameworkContractAnno != null) { - result.addAll(getContract(kind, frameworkContractAnno, clazz, methodDecl)); + result.addAll(getContract(kind, frameworkContractAnno, clazz)); } // Check for a framework-defined wrapper around contract annotations. @@ -152,7 +139,7 @@ private Set getContractsOfKind( List frameworkContractAnnoList = factory.getContractListValues(frameworkContractListAnno); for (AnnotationMirror a : frameworkContractAnnoList) { - result.addAll(getContract(kind, a, clazz, methodDecl)); + result.addAll(getContract(kind, a, clazz)); } } @@ -166,18 +153,16 @@ private Set getContractsOfKind( // contractAnno is the meta-annotation on anno, such as PreconditionAnnotation, // PostconditionAnnotation, or ConditionalPostconditionAnnotation. AnnotationMirror contractAnno = r.second; + AnnotationMirror enforcedQualifier = + getQualifierEnforcedByContractAnnotation(contractAnno, anno); + if (enforcedQualifier == null) { + continue; + } List expressions = factory.getContractExpressions(kind, anno); Collections.sort(expressions); Boolean ensuresQualifierIfResult = factory.getEnsuresQualifierIfResult(kind, anno); for (String expr : expressions) { - TypeMirror exprType = getExprType(expr, methodDecl); - AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnno, anno, exprType); - if (enforcedQualifier == null) { - continue; - } - T contract = clazz.cast( Contract.create(kind, expr, enforcedQualifier, anno, ensuresQualifierIfResult)); @@ -187,27 +172,6 @@ private Set getContractsOfKind( return result; } - /** - * Returns the type of the given expression. - * - * @param expr an expression - * @param methodDecl the method at which to interpret the expression - * @return the type of the given expression - */ - private @Nullable TypeMirror getExprType(String expr, @Nullable MethodTree methodDecl) { - if (methodDecl == null) { - return null; - } else { - try { - JavaExpression jExpr = - StringToJavaExpression.atMethodBody(expr, methodDecl, factory.getChecker()); - return jExpr.getType(); - } catch (JavaExpressionParseException e) { - return null; - } - } - } - /** * Returns the contracts expressed by the given framework contract annotation. * @@ -216,19 +180,21 @@ private Set getContractsOfKind( * @param contractAnnotation a {@link RequiresQualifier}, {@link EnsuresQualifier}, or {@link * EnsuresQualifierIf} * @param clazz the class to determine the return type - * @param methodDecl the method on which the contract is written * @return the contracts expressed by the given annotation, or the empty set if the argument is * null */ private Set getContract( - Contract.Kind kind, - AnnotationMirror contractAnnotation, - Class clazz, - @Nullable MethodTree methodDecl) { + Contract.Kind kind, AnnotationMirror contractAnnotation, Class clazz) { if (contractAnnotation == null) { return Collections.emptySet(); } + AnnotationMirror enforcedQualifier = + getQualifierEnforcedByContractAnnotation(contractAnnotation); + if (enforcedQualifier == null) { + return Collections.emptySet(); + } + List expressions = factory.getContractExpressions(contractAnnotation); Collections.sort(expressions); @@ -237,13 +203,6 @@ private Set getContract( Set result = new LinkedHashSet<>(); for (String expr : expressions) { - TypeMirror exprType = getExprType(expr, methodDecl); - AnnotationMirror enforcedQualifier = - getQualifierEnforcedByContractAnnotation(contractAnnotation, null, exprType); - if (enforcedQualifier == null) { - continue; - } - T contract = clazz.cast( Contract.create( @@ -253,26 +212,32 @@ private Set getContract( return result; } + /** + * Returns the annotation mirror as specified by the {@code qualifier} element in {@code + * contractAnno}. May return null. + * + * @param contractAnno a pre- or post-condition annotation, such as {@code @RequiresQualifier} + * @return the type annotation specified in {@code contractAnno.qualifier} + */ + private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( + AnnotationMirror contractAnno) { + return getQualifierEnforcedByContractAnnotation(contractAnno, null, null); + } + /** * Returns the annotation mirror as specified by the {@code qualifier} element in {@code * contractAnno}, with elements/arguments taken from {@code argumentAnno}. May return null. * * @param contractAnno a pre- or post-condition annotation, such as {@code @RequiresQualifier} * @param argumentAnno supplies the elements/fields in the return value - * @param type the type to which the annotation applies * @return the type annotation specified in {@code contractAnno.qualifier} */ private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( - AnnotationMirror contractAnno, - @Nullable AnnotationMirror argumentAnno, - @Nullable TypeMirror type) { + AnnotationMirror contractAnno, AnnotationMirror argumentAnno) { Map argumentRenaming = - argumentAnno == null - ? Collections.emptyMap() - : makeArgumentRenaming(argumentAnno.getAnnotationType().asElement()); - return getQualifierEnforcedByContractAnnotation( - contractAnno, argumentAnno, argumentRenaming, type); + makeArgumentRenaming(argumentAnno.getAnnotationType().asElement()); + return getQualifierEnforcedByContractAnnotation(contractAnno, argumentAnno, argumentRenaming); } /** @@ -289,15 +254,13 @@ private Set getContract( * @param argumentAnno annotation containing the element {@code values}, or {@code null} * @param argumentRenaming renaming of argument names, which maps from names in {@code * argumentAnno} to names used in the returned annotation, or {@code null} - * @param type the type to which the annotation applies * @return a qualifier whose type is that of {@code contractAnno.qualifier}, or an alias for it, * or null if it is not a supported qualifier of the type system */ private @Nullable AnnotationMirror getQualifierEnforcedByContractAnnotation( AnnotationMirror contractAnno, @Nullable AnnotationMirror argumentAnno, - Map argumentRenaming, - @Nullable TypeMirror type) { + @Nullable Map argumentRenaming) { @SuppressWarnings("deprecation") // permitted for use in the framework Name c = AnnotationUtils.getElementValueClassName(contractAnno, "qualifier", false); @@ -312,7 +275,7 @@ private Set getContract( anno = builder.build(); } - anno = factory.canonicalAnnotation(anno, type); + anno = factory.canonicalAnnotation(anno); if (factory.isSupportedQualifier(anno)) { return anno; } else { From f410159072410a70c7f0489dfdcf319381bfca95 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 15 May 2026 15:17:38 -0700 Subject: [PATCH 165/308] add test for @IteratorPolyMod on method parameters --- .../modifiability/BasicModifiabilityTest.java | 3 +- .../modifiability/IteratorPrecisionTest.java | 185 ++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 69f7fb6f3291..cfc347030cef 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -1,6 +1,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -100,7 +101,7 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List unmod.retainAll(other); } - void testReplaceAll(@Modifiable List mod, @Unmodifiable List unmod) { + void testReplaceAll(@IteratorPolyMod @Modifiable List mod, @Unmodifiable List unmod) { mod.replaceAll(s -> s + "!"); // :: error: [method.invocation] diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 72eb54f0b9e8..8b1592beca54 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -1,3 +1,6 @@ +import java.util.AbstractCollection; +import java.util.AbstractSequentialList; +import java.util.AbstractSet; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -13,8 +16,10 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.NoSuchElementException; import java.util.PriorityQueue; import java.util.Queue; +import java.util.Random; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ArrayBlockingQueue; @@ -239,4 +244,184 @@ void unmodifiableMapWrapperViewIteratorsDoNotPreserveRemove() { @Unshrinkable Iterator valueIterator = map.values().iterator(); @Unshrinkable Iterator> entryIterator = map.entrySet().iterator(); } + + void iteratorDependentCollectionMethodsRequireIteratorPolyMod( + @Modifiable IteratorRemoveUnsupportedCollection collection, + @Modifiable IteratorRemoveUnsupportedSet set) { + // :: error: [method.invocation] + collection.remove("a"); + // :: error: [method.invocation] + collection.removeAll(List.of("a")); + // :: error: [method.invocation] + collection.retainAll(List.of("a")); + // :: error: [method.invocation] + collection.clear(); + // :: error: [method.invocation] + collection.removeIf(s -> true); + + // :: error: [method.invocation] + set.removeAll(List.of("a")); + } + + void iteratorDependentListMethodsRequireIteratorPolyMod( + @Modifiable IteratorMutationUnsupportedList list) { + // :: error: [method.invocation] + list.set(0, "b"); + // :: error: [method.invocation] + list.add(0, "b"); + // :: error: [method.invocation] + list.remove(0); + // :: error: [method.invocation] + list.addAll(0, List.of("b")); + // :: error: [method.invocation] + list.replaceAll(s -> s + "!"); + // :: error: [method.invocation] + list.sort(null); + + // :: error: [argument] + Collections.sort(list); + // :: error: [argument] + Collections.sort(list, String::compareTo); + // :: error: [argument] + Collections.reverse(list); + // :: error: [argument] + Collections.shuffle(list); + // :: error: [argument] + Collections.shuffle(list, new Random()); + // :: error: [argument] + Collections.fill(list, "b"); + // :: error: [argument] + Collections.copy(list, List.of("b")); + // :: error: [argument] + Collections.rotate(list, 1); + // :: error: [argument] + Collections.replaceAll(list, "a", "b"); + } + + void copyOnWriteDirectOverridesRemainUsableButInheritedIteratorBasedApisDoNot() { + CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); + list.add("a"); + list.removeIf(s -> false); + list.replaceAll(s -> s + "!"); + list.sort(null); + list.clear(); + + // :: error: [argument] + Collections.sort(list); + // :: error: [argument] + Collections.reverse(list); + + List asList = list; + // :: error: [method.invocation] + asList.sort(null); + // :: error: [method.invocation] + asList.replaceAll(s -> s); + } + + static class IteratorRemoveUnsupportedCollection extends AbstractCollection { + @Override + public Iterator iterator() { + return new Iterator<>() { + private boolean hasNext = true; + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public String next() { + if (!hasNext) { + throw new NoSuchElementException(); + } + hasNext = false; + return "a"; + } + }; + } + + @Override + public int size() { + return 1; + } + } + + static class IteratorRemoveUnsupportedSet extends AbstractSet { + @Override + public Iterator iterator() { + return List.of("a").iterator(); + } + + @Override + public int size() { + return 1; + } + } + + static class IteratorMutationUnsupportedList extends AbstractSequentialList { + @Override + public ListIterator listIterator(int index) { + return new ListIterator<>() { + private int cursor = index; + + @Override + public boolean hasNext() { + return cursor < 1; + } + + @Override + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + cursor++; + return "a"; + } + + @Override + public boolean hasPrevious() { + return cursor > 0; + } + + @Override + public String previous() { + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + cursor--; + return "a"; + } + + @Override + public int nextIndex() { + return cursor; + } + + @Override + public int previousIndex() { + return cursor - 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void set(String e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(String e) { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return 1; + } + } } From eeb038915d5a35d92745c73a7cb81ea829c1cb73 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 15 May 2026 15:22:18 -0700 Subject: [PATCH 166/308] spotlessApply --- checker/tests/modifiability/BasicModifiabilityTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index cfc347030cef..075c1143191c 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -101,7 +101,8 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List unmod.retainAll(other); } - void testReplaceAll(@IteratorPolyMod @Modifiable List mod, @Unmodifiable List unmod) { + void testReplaceAll( + @IteratorPolyMod @Modifiable List mod, @Unmodifiable List unmod) { mod.replaceAll(s -> s + "!"); // :: error: [method.invocation] From b2efced4331fc48c89ff97ddd7381cbe8d628a04 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 17 May 2026 17:57:47 -0700 Subject: [PATCH 167/308] add iteratorPolyMod annotations to stub files. --- .../checker/modifiability/grow/javaparser.astub | 3 ++- .../checker/modifiability/replace/javaparser.astub | 3 ++- .../checker/modifiability/shrink/javaparser.astub | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub index cc63c87a8309..dadc1d1762fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub @@ -1,4 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; @@ -9,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable NodeList getImports() {} + public @Modifiable @IteratorPolyMod NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub index cc63c87a8309..dadc1d1762fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub @@ -1,4 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; @@ -9,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable NodeList getImports() {} + public @Modifiable @IteratorPolyMod NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub index cc63c87a8309..dadc1d1762fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub @@ -1,4 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; @@ -9,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable NodeList getImports() {} + public @Modifiable @IteratorPolyMod NodeList getImports() {} } From 2dd5a6499028792b00379cfadb1e3f196a728ddb Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 17 May 2026 21:53:32 -0700 Subject: [PATCH 168/308] add iteratorPolyMod to collectionsPlume.withoutDuplicates. Co-authored-by: Copilot --- .../modifiability/grow/GrowAnnotatedTypeFactory.java | 5 +++-- .../replace/ReplaceAnnotatedTypeFactory.java | 5 +++-- .../modifiability/shrink/ShrinkAnnotatedTypeFactory.java | 9 ++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 644eac8504e6..211ac59b99bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -173,8 +173,9 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(GROWABLE)) { methodType.getReturnType().replaceAnnotation(GROWABLE); - } else { - methodType.getReturnType().replaceAnnotation(UNKNOWN_GROW); + } + if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 5f67f72d8f09..d9faee993f53 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -188,8 +188,9 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(REPLACEABLE)) { methodType.getReturnType().replaceAnnotation(REPLACEABLE); - } else { - methodType.getReturnType().replaceAnnotation(UNKNOWN_REPLACE); + } + if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index cb20fbe681ef..591809d389be 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -168,9 +168,12 @@ protected ParameterizedExecutableType methodFromUse( private void refineCollectionsPlumeWithoutDuplicatesReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); - AnnotationMirror returnAnnotation = - argumentType.hasPrimaryAnnotation(SHRINKABLE) ? SHRINKABLE : UNKNOWN_SHRINK; - methodType.getReturnType().replaceAnnotation(returnAnnotation); + if (argumentType.hasPrimaryAnnotation(SHRINKABLE)) { + methodType.getReturnType().replaceAnnotation(SHRINKABLE); + } + if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); + } } /** From 4b981523093266ba827f3b8a5e70e34bc3577362 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 17 May 2026 23:57:29 -0700 Subject: [PATCH 169/308] rename unknownMod to be MaybeMod --- buildSrc/src/main/groovy/cf-spotless.gradle | 10 +-- .../checker/modifiability/qual/Growable.java | 2 +- .../modifiability/qual/IteratorPolyMod.java | 4 +- .../qual/{UnknownGrow.java => MaybeGrow.java} | 2 +- ...PolyMod.java => MaybeIteratorPolyMod.java} | 2 +- ...odifiability.java => MaybeModifiable.java} | 10 +-- ...{UnknownReplace.java => MaybeReplace.java} | 2 +- .../{UnknownShrink.java => MaybeShrink.java} | 2 +- .../modifiability/qual/Modifiable.java | 4 +- .../checker/modifiability/qual/PolyGrow.java | 2 +- .../modifiability/qual/PolyReplace.java | 2 +- .../modifiability/qual/PolyShrink.java | 2 +- .../modifiability/qual/Replaceable.java | 2 +- .../modifiability/qual/Shrinkable.java | 2 +- .../modifiability/qual/Ungrowable.java | 2 +- .../modifiability/qual/Unmodifiable.java | 4 +- .../modifiability/qual/Unreplaceable.java | 2 +- .../modifiability/qual/Unshrinkable.java | 2 +- .../modifiability/ModifiabilityVisitor.java | 2 +- .../grow/GrowAnnotatedTypeFactory.java | 36 ++++----- .../modifiability/grow/GrowChecker.java | 4 +- .../replace/ReplaceAnnotatedTypeFactory.java | 32 ++++---- .../modifiability/replace/ReplaceChecker.java | 2 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 43 +++++----- .../modifiability/shrink/ShrinkChecker.java | 2 +- .../AliasCanonicalizationTest.java | 18 ++--- .../modifiability/BasicModifiabilityTest.java | 6 +- .../CollectionModifiableTest.java | 4 +- .../tests/modifiability/HierarchyTest.java | 66 ++++++++-------- .../modifiability/IteratorPrecisionTest.java | 2 +- .../modifiability/MapModifiableTest.java | 4 +- .../modifiability/PolyModifiableTest.java | 54 ++++++------- .../modifiability/SetRefinementTest.java | 4 +- checker/tests/modifiability/SetTest.java | 79 +++++++++---------- docs/manual/modifiability-checker.tex | 54 ++++++------- 35 files changed, 234 insertions(+), 236 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownGrow.java => MaybeGrow.java} (96%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownIteratorPolyMod.java => MaybeIteratorPolyMod.java} (94%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownModifiability.java => MaybeModifiable.java} (60%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownReplace.java => MaybeReplace.java} (96%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnknownShrink.java => MaybeShrink.java} (96%) diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 0fc1bff209c4..65ea1476bea7 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -105,11 +105,11 @@ spotless { .addTypeAnnotation("Replaceable") .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") - .addTypeAnnotation("UnknownGrow") - .addTypeAnnotation("UnknownIteratorPolyMod") - .addTypeAnnotation("UnknownModifiability") - .addTypeAnnotation("UnknownReplace") - .addTypeAnnotation("UnknownShrink") + .addTypeAnnotation("MaybeGrow") + .addTypeAnnotation("MaybeIteratorPolyMod") + .addTypeAnnotation("MaybeModifiable") + .addTypeAnnotation("MaybeReplace") + .addTypeAnnotation("MaybeShrink") .addTypeAnnotation("Unmodifiable") .addTypeAnnotation("Unreplaceable") .addTypeAnnotation("Unshrinkable") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java index 5c2b1b7315c1..2c8041469c75 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownGrow.class) +@SubtypeOf(MaybeGrow.class) public @interface Growable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index d5d8ef9253b7..8ad422162e21 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -11,7 +11,7 @@ * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} method preserves * the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has type * {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any - * collection whose type is {@code @UnknownIteratorPolyMod}, its iterator is always + * collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is always * {@code @Unshrinkable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownIteratorPolyMod.class) +@SubtypeOf(MaybeIteratorPolyMod.class) public @interface IteratorPolyMod {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrow.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrow.java index c71e04417b1c..92789632cc75 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownGrow.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrow.java @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownGrow {} +public @interface MaybeGrow {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java similarity index 94% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyMod.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java index 4c359545002e..30f794ba7973 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownIteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownIteratorPolyMod {} +public @interface MaybeIteratorPolyMod {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java similarity index 60% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java index 502805c4a134..5a9a53e8f45c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java @@ -7,16 +7,16 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @UnknownGrow @UnknownShrink @UnknownReplace}. Represents an - * unknown or arbitrary modifiability capability; the checker cannot determine whether the - * collection is growable, shrinkable, or replaceable. + * Convenience alias meaning {@code @MaybeGrow @MaybeShrink @MaybeReplace}. Represents an unknown or + * arbitrary modifiability capability; the checker cannot determine whether the collection is + * growable, shrinkable, or replaceable. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @UnknownGrow @UnknownShrink @UnknownReplace} on each annotated type. + * {@code @MaybeGrow @MaybeShrink @MaybeReplace} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -public @interface UnknownModifiability {} +public @interface MaybeModifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplace.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplace.java index 164002371c5b..70fbbb939007 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplace.java @@ -22,4 +22,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownReplace {} +public @interface MaybeReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrink.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrink.java index 68f5918995e3..d5edc2ccf523 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnknownShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrink.java @@ -22,4 +22,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface UnknownShrink {} +public @interface MaybeShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 861c92453b59..2d5f571deb64 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -11,9 +11,9 @@ * operation (growing, shrinking, or replacing) on this collection will not result in throwing * {@link UnsupportedOperationException}. * - *

As an exception, {@code @Modifiable} means {@code @Unknown*} if the type does not support a + *

As an exception, {@code @Modifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Modifiable Iterator} means - * {@code @UnknownGrow @Shrinkable @UnknownReplace Iterator}. + * {@code @MaybeGrow @Shrinkable @MaybeReplace Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java index 3a4d5b43fbf8..ef00276e8d64 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(UnknownGrow.class) +@PolymorphicQualifier(MaybeGrow.class) public @interface PolyGrow {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java index eb6e7ac5af7d..f8664f330bca 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(UnknownReplace.class) +@PolymorphicQualifier(MaybeReplace.class) public @interface PolyReplace {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java index b491d8626f99..f35eece7403b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(UnknownShrink.class) +@PolymorphicQualifier(MaybeShrink.class) public @interface PolyShrink {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java index bc6199b61f62..2d7c2e7c6926 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownReplace.class) +@SubtypeOf(MaybeReplace.class) public @interface Replaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java index cabe5a275869..1d7fddbeea03 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownShrink.class) +@SubtypeOf(MaybeShrink.class) public @interface Shrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java index 24348a29d853..d160c345b0e4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownGrow.class) +@SubtypeOf(MaybeGrow.class) public @interface Ungrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 37618672a08a..53ea1e4066ff 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -11,9 +11,9 @@ * mutating operation (growing, shrinking, or replacing) may throw {@link * UnsupportedOperationException}. * - *

As an exception, {@code @Unmodifiable} means {@code @Unknown*} if the type does not support a + *

As an exception, {@code @Unmodifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Unmodifiable Iterator} means - * {@code @UnknownGrow @Unshrinkable @UnknownReplace Iterator}. + * {@code @MaybeGrow @Unshrinkable @MaybeReplace Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java index 1f58bc15625f..32dc1f2e6a47 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownReplace.class) +@SubtypeOf(MaybeReplace.class) public @interface Unreplaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java index 1b97e46abbdf..347db2a2b656 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownShrink.class) +@SubtypeOf(MaybeShrink.class) public @interface Unshrinkable {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 8dd99931477e..ce7e0432705a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -63,7 +63,7 @@ protected boolean shouldCheckThrowsUOE() { // Suppresses the framework's "constructor result must be TOP" check. // Collection constructors (e.g., new ArrayList()) legitimately produce @Modifiable, which is a - // subtype of the top type @UnknownModifiability. This suppression is sound: constructors are + // subtype of the top type @MaybeModifiable. This suppression is sound: constructors are // typed by their stubs or by defaults inferred from the class's annotations, so they cannot // silently widen an unmodifiable collection to @Modifiable. @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 211ac59b99bc..79bc52145978 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -15,13 +15,13 @@ import org.checkerframework.checker.modifiability.qual.BottomGrow; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeGrow; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrow; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Ungrowable; -import org.checkerframework.checker.modifiability.qual.UnknownGrow; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -47,8 +47,8 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link UnknownGrow} qualifier (top of Grow hierarchy). */ - private final AnnotationMirror UNKNOWN_GROW; + /** The {@code @}{@link MaybeGrow} qualifier (top of Grow hierarchy). */ + private final AnnotationMirror MAYBE_GROW; /** The {@code @}{@link Growable} qualifier. */ private final AnnotationMirror GROWABLE; @@ -79,7 +79,7 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.listIteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.UNKNOWN_GROW = AnnotationBuilder.fromClass(getElementUtils(), UnknownGrow.class); + this.MAYBE_GROW = AnnotationBuilder.fromClass(getElementUtils(), MaybeGrow.class); this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); @@ -93,12 +93,12 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - UnknownGrow.class, + MaybeGrow.class, Growable.class, Ungrowable.class, BottomGrow.class, PolyGrow.class, - UnknownIteratorPolyMod.class, + MaybeIteratorPolyMod.class, IteratorPolyMod.class)); } @@ -108,25 +108,25 @@ protected Set> createSupportedTypeQualifiers() { * *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that cannot grow structurally, such as {@code Map.Entry} and non-{@code ListIterator} {@code - * Iterator}, their grow component canonicalizes to {@code @UnknownGrow}. Explicit grow qualifiers + * Iterator}, their grow component canonicalizes to {@code @MaybeGrow}. Explicit grow qualifiers * are left to normal canonicalization so users can still write and preserve an explicit * capability tuple such as {@code @Growable @Shrinkable @Replaceable Iterator}. * *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrow}, but for * {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its grow - * bit is therefore {@code @UnknownGrow}. + * bit is therefore {@code @MaybeGrow}. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotGrow(tm) ? UNKNOWN_GROW : GROWABLE; + return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : GROWABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotGrow(tm) ? UNKNOWN_GROW : UNGROWABLE; - } else if (areSameByClass(annotation, UnknownModifiability.class)) { - return UNKNOWN_GROW; + return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : UNGROWABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class)) { + return MAYBE_GROW; } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && isMapEntry(tm) ? UNKNOWN_GROW : POLY_GROW; + return tm != null && isMapEntry(tm) ? MAYBE_GROW : POLY_GROW; } return super.canonicalAnnotation(annotation); } @@ -161,9 +161,9 @@ protected ParameterizedExecutableType methodFromUse( * * would be unsound, because an {@code @Ungrowable} input could receive the fresh ArrayList, whose * static type should be {@code @Growable}. It would also be too imprecise to always use - * {@code @UnknownGrow}, because passing a {@code @Growable} collection guarantees that both + * {@code @MaybeGrow}, because passing a {@code @Growable} collection guarantees that both * possible results are growable. Therefore, model the method here as preserving {@code @Growable} - * inputs and otherwise returning {@code @UnknownGrow}. + * inputs and otherwise returning {@code @MaybeGrow}. * * @param tree the invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method @@ -212,7 +212,7 @@ private void refineListIteratorReturnType( if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(GROWABLE); } else { - returnType.replaceAnnotation(UNKNOWN_GROW); + returnType.replaceAnnotation(MAYBE_GROW); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 004f9eae9f1a..cefbe5f6d2e0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a grow method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Growable} collections can be - * safely added to, {@code @Ungrowable} collections cannot, and {@code @UnknownGrow} collections - * have unknown grow behavior. + * safely added to, {@code @Ungrowable} collections cannot, and {@code @MaybeGrow} collections have + * unknown grow behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index d9faee993f53..638f24df258e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -14,13 +14,13 @@ import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplace; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.MaybeReplace; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplace; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; -import org.checkerframework.checker.modifiability.qual.UnknownReplace; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.nullness.qual.Nullable; @@ -56,8 +56,8 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link UnknownReplace} qualifier (top of Replace hierarchy). */ - private final AnnotationMirror UNKNOWN_REPLACE; + /** The {@code @}{@link MaybeReplace} qualifier (top of Replace hierarchy). */ + private final AnnotationMirror MAYBE_REPLACE; /** The {@code @}{@link Replaceable} qualifier. */ private final AnnotationMirror REPLACEABLE; @@ -93,7 +93,7 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.UNKNOWN_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), UnknownReplace.class); + this.MAYBE_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), MaybeReplace.class); this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); @@ -107,12 +107,12 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - UnknownReplace.class, + MaybeReplace.class, Replaceable.class, Unreplaceable.class, BottomReplace.class, PolyReplace.class, - UnknownIteratorPolyMod.class, + MaybeIteratorPolyMod.class, IteratorPolyMod.class)); } @@ -123,7 +123,7 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that cannot replace structurally, such as exact {@code Collection}, {@code Set}, non-{@code * LinkedList} {@code Queue}, and non-{@code ListIterator} {@code Iterator}, their replace - * component canonicalizes to {@code @UnknownReplace}. Explicit replace qualifiers are left to + * component canonicalizes to {@code @MaybeReplace}. Explicit replace qualifiers are left to * normal canonicalization so users can still write and preserve an explicit capability tuple such * as {@code @Growable @Shrinkable @Replaceable Set}. * @@ -135,11 +135,11 @@ protected Set> createSupportedTypeQualifiers() { public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotReplace(tm) ? UNKNOWN_REPLACE : REPLACEABLE; + return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : REPLACEABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotReplace(tm) ? UNKNOWN_REPLACE : UNREPLACEABLE; - } else if (areSameByClass(annotation, UnknownModifiability.class)) { - return UNKNOWN_REPLACE; + return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : UNREPLACEABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class)) { + return MAYBE_REPLACE; } else if (areSameByClass(annotation, PolyModifiable.class)) { return POLY_REPLACE; } @@ -176,9 +176,9 @@ protected ParameterizedExecutableType methodFromUse( * * would be unsound, because an {@code @Unreplaceable} input could receive the fresh ArrayList, * whose static type should be {@code @Replaceable}. It would also be too imprecise to always use - * {@code @UnknownReplace}, because passing a {@code @Replaceable} collection guarantees that both + * {@code @MaybeReplace}, because passing a {@code @Replaceable} collection guarantees that both * possible results are replaceable. Therefore, model the method here as preserving - * {@code @Replaceable} inputs and otherwise returning {@code @UnknownReplace}. + * {@code @Replaceable} inputs and otherwise returning {@code @MaybeReplace}. * * @param tree the invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method @@ -227,7 +227,7 @@ private void refineListIteratorReturnType( if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(REPLACEABLE); } else { - returnType.replaceAnnotation(UNKNOWN_REPLACE); + returnType.replaceAnnotation(MAYBE_REPLACE); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index f3367574758d..f5a38440590c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -15,7 +15,7 @@ * *

The checker enforces the Modifiability type system, where {@code @Replaceable} collections can * be safely mutated, {@code @Unreplaceable} collections cannot be replaced, and - * {@code @UnknownReplace} collections have unknown replace behavior. + * {@code @MaybeReplace} collections have unknown replace behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 591809d389be..f28c0bd9670f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -14,13 +14,13 @@ import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrink; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.MaybeShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownIteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; -import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.checker.nullness.qual.Nullable; @@ -45,8 +45,8 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link UnknownShrink} qualifier (top of Shrink hierarchy). */ - private final AnnotationMirror UNKNOWN_SHRINK; + /** The {@code @}{@link MaybeShrink} qualifier (top of Shrink hierarchy). */ + private final AnnotationMirror MAYBE_SHRINK; /** The {@code @}{@link Shrinkable} qualifier. */ private final AnnotationMirror SHRINKABLE; @@ -76,7 +76,7 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.UNKNOWN_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), UnknownShrink.class); + this.MAYBE_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrink.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); @@ -90,12 +90,12 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - UnknownShrink.class, + MaybeShrink.class, Shrinkable.class, Unshrinkable.class, BottomShrink.class, PolyShrink.class, - UnknownIteratorPolyMod.class, + MaybeIteratorPolyMod.class, IteratorPolyMod.class)); } @@ -104,26 +104,25 @@ protected Set> createSupportedTypeQualifiers() { * aliases whose meaning depends on the annotated type. * *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on {@code - * Map.Entry}, which cannot shrink, their shrink component canonicalizes to - * {@code @UnknownShrink}. Explicit shrink qualifiers are left to normal canonicalization so users - * can still write and preserve an explicit capability tuple such as - * {@code @Growable @Shrinkable @Replaceable Entry}. + * Map.Entry}, which cannot shrink, their shrink component canonicalizes to {@code @MaybeShrink}. + * Explicit shrink qualifiers are left to normal canonicalization so users can still write and + * preserve an explicit capability tuple such as {@code @Growable @Shrinkable @Replaceable Entry}. * *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrink}, but for * {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its shrink - * bit is therefore {@code @UnknownShrink}. + * bit is therefore {@code @MaybeShrink}. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotShrink(tm) ? UNKNOWN_SHRINK : SHRINKABLE; + return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : SHRINKABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotShrink(tm) ? UNKNOWN_SHRINK : UNSHRINKABLE; - } else if (areSameByClass(annotation, UnknownModifiability.class)) { - return UNKNOWN_SHRINK; + return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : UNSHRINKABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class)) { + return MAYBE_SHRINK; } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && isMapEntry(tm) ? UNKNOWN_SHRINK : POLY_SHRINK; + return tm != null && isMapEntry(tm) ? MAYBE_SHRINK : POLY_SHRINK; } return super.canonicalAnnotation(annotation); } @@ -158,9 +157,9 @@ protected ParameterizedExecutableType methodFromUse( * * would be unsound, because an {@code @Unshrinkable} input could receive the fresh ArrayList, * whose static type should be {@code @Shrinkable}. It would also be too imprecise to always use - * {@code @UnknownShrink}, because passing a {@code @Shrinkable} collection guarantees that both + * {@code @MaybeShrink}, because passing a {@code @Shrinkable} collection guarantees that both * possible results are shrinkable. Therefore, model the method here as preserving - * {@code @Shrinkable} inputs and otherwise returning {@code @UnknownShrink}. + * {@code @Shrinkable} inputs and otherwise returning {@code @MaybeShrink}. * * @param tree the invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method @@ -181,7 +180,7 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( * *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked * method receiver type has {@code @IteratorPolyMod}, then the result is {@code @Shrinkable - * Iterator}. Otherwise, shrinkability precision is dropped to {@code @UnknownShrink}. + * Iterator}. Otherwise, shrinkability precision is dropped to {@code @MaybeShrink}. * * @param tree the iterator method invocation * @param methodType the annotated executable type of the invoked method @@ -216,7 +215,7 @@ private void refineIteratorReturnType( if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { - returnType.replaceAnnotation(UNKNOWN_SHRINK); + returnType.replaceAnnotation(MAYBE_SHRINK); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index ab946674e35a..0e5894ee4ea3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -14,7 +14,7 @@ * UnsupportedOperationException} at run time due to calling a shrink method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Shrinkable} collections can - * be safely mutated, {@code @Unshrinkable} collections cannot be shrunk, and {@code @UnknownShrink} + * be safely mutated, {@code @Unshrinkable} collections cannot be shrunk, and {@code @MaybeShrink} * collections have unknown shrink behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker diff --git a/checker/tests/modifiability/AliasCanonicalizationTest.java b/checker/tests/modifiability/AliasCanonicalizationTest.java index 97e4803a18b2..88ae53220063 100644 --- a/checker/tests/modifiability/AliasCanonicalizationTest.java +++ b/checker/tests/modifiability/AliasCanonicalizationTest.java @@ -5,49 +5,49 @@ import java.util.Queue; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeGrow; +import org.checkerframework.checker.modifiability.qual.MaybeReplace; +import org.checkerframework.checker.modifiability.qual.MaybeShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownGrow; -import org.checkerframework.checker.modifiability.qual.UnknownReplace; -import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; class AliasCanonicalizationTest { void modifiableIterator(@Modifiable Iterator iterator) { - @UnknownGrow @Shrinkable @UnknownReplace Iterator canonical = iterator; + @MaybeGrow @Shrinkable @MaybeReplace Iterator canonical = iterator; @Shrinkable Iterator shrinkable = iterator; iterator.remove(); } void unmodifiableIterator(@Unmodifiable Iterator iterator) { - @UnknownGrow @Unshrinkable @UnknownReplace Iterator canonical = iterator; + @MaybeGrow @Unshrinkable @MaybeReplace Iterator canonical = iterator; // :: error: [method.invocation] iterator.remove(); } void modifiableSet(@Modifiable Set set) { - @Growable @Shrinkable @UnknownReplace Set canonical = set; + @Growable @Shrinkable @MaybeReplace Set canonical = set; set.add("a"); set.remove("a"); } void modifiableQueue(@Modifiable Queue queue) { - @Growable @Shrinkable @UnknownReplace Queue canonical = queue; + @Growable @Shrinkable @MaybeReplace Queue canonical = queue; queue.add("a"); queue.remove(); } void modifiableCollection(@Modifiable Collection collection) { - @Growable @Shrinkable @UnknownReplace Collection canonical = collection; + @Growable @Shrinkable @MaybeReplace Collection canonical = collection; collection.add("a"); collection.remove("a"); } void modifiableEntry(Map.@Modifiable Entry entry) { - Map.@UnknownGrow @UnknownShrink @Replaceable Entry canonical = entry; + Map.@MaybeGrow @MaybeShrink @Replaceable Entry canonical = entry; entry.setValue("value"); } diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 075c1143191c..175ad8461d6a 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -2,8 +2,8 @@ import java.util.Iterator; import java.util.List; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; // this checkes the correctness of annotation in List.java and ONLY the initialization of ArrayList @@ -61,7 +61,7 @@ void testModifiableFactoryMethods() { void testToArrayModifiability( @Modifiable List modList, @Unmodifiable List unmodList, - @UnknownModifiability List anyList) { + @MaybeModifiable List anyList) { modList.toArray(); unmodList.toArray(); @@ -75,7 +75,7 @@ void testToArrayModifiability( void testIteratorPolymorphic( @Modifiable List modList, @Unmodifiable List unmodList) { - @UnknownModifiability Iterator itMod = modList.iterator(); + @MaybeModifiable Iterator itMod = modList.iterator(); // And the iterator from an unmodifiable list is unmodifiable: @Unmodifiable Iterator itUnmod = unmodList.iterator(); diff --git a/checker/tests/modifiability/CollectionModifiableTest.java b/checker/tests/modifiability/CollectionModifiableTest.java index 01f458bf19c4..89ae8faab908 100644 --- a/checker/tests/modifiability/CollectionModifiableTest.java +++ b/checker/tests/modifiability/CollectionModifiableTest.java @@ -3,8 +3,8 @@ import java.util.List; import java.util.Spliterator; import java.util.stream.Stream; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; // Tests for Collection mutating and read-only methods with modifiability qualifiers. @@ -72,7 +72,7 @@ void testFactoryAssignments() { void testAnnotatedLocalsFromParams( @Modifiable Collection mod, @Unmodifiable Collection unmod, - @UnknownModifiability Collection unknown) { + @MaybeModifiable Collection unknown) { mod.add("p"); // :: error: [method.invocation] unmod.add("p"); diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index d412890cde4c..2eaf9158d8dd 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -2,14 +2,14 @@ import org.checkerframework.checker.modifiability.qual.BottomReplace; import org.checkerframework.checker.modifiability.qual.BottomShrink; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeGrow; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.MaybeReplace; +import org.checkerframework.checker.modifiability.qual.MaybeShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Ungrowable; -import org.checkerframework.checker.modifiability.qual.UnknownGrow; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; -import org.checkerframework.checker.modifiability.qual.UnknownReplace; -import org.checkerframework.checker.modifiability.qual.UnknownShrink; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; @@ -17,7 +17,7 @@ /** * Tests the three independent 4-element lattices of the Modifiability Checker. * - *

Each hierarchy is: Unknown* (top/default), two incomparable siblings (*able and Un*able), and + *

Each hierarchy is: Maybe* (top/default), two incomparable siblings (*able and Un*able), and * Bottom* (bottom). * *

An annotated type carries one qualifier per hierarchy. Assignment is valid if and only if the @@ -26,10 +26,10 @@ class HierarchyTest { void testGrow( - @Growable Object g, @Ungrowable Object ug, @UnknownGrow Object u, @BottomGrow Object b) { - @UnknownGrow Object u1 = g; - @UnknownGrow Object u2 = ug; - @UnknownGrow Object u3 = b; + @Growable Object g, @Ungrowable Object ug, @MaybeGrow Object u, @BottomGrow Object b) { + @MaybeGrow Object u1 = g; + @MaybeGrow Object u2 = ug; + @MaybeGrow Object u3 = b; @Growable Object g1 = b; @Ungrowable Object ug1 = b; @@ -53,11 +53,11 @@ void testGrow( void testShrink( @Shrinkable Object s, @Unshrinkable Object us, - @UnknownShrink Object u, + @MaybeShrink Object u, @BottomShrink Object b) { - @UnknownShrink Object u1 = s; - @UnknownShrink Object u2 = us; - @UnknownShrink Object u3 = b; + @MaybeShrink Object u1 = s; + @MaybeShrink Object u2 = us; + @MaybeShrink Object u3 = b; @Shrinkable Object s1 = b; @Unshrinkable Object us1 = b; @@ -75,11 +75,11 @@ void testShrink( void testReplace( @Replaceable Object r, @Unreplaceable Object ur, - @UnknownReplace Object u, + @MaybeReplace Object u, @BottomReplace Object b) { - @UnknownReplace Object u1 = r; - @UnknownReplace Object u2 = ur; - @UnknownReplace Object u3 = b; + @MaybeReplace Object u1 = r; + @MaybeReplace Object u2 = ur; + @MaybeReplace Object u3 = b; @Replaceable Object r1 = b; @Unreplaceable Object ur1 = b; @@ -103,7 +103,7 @@ void testCombinations( @Shrinkable Object s, @Replaceable Object r, @Ungrowable @Unshrinkable @Unreplaceable Object none, - @UnknownGrow @UnknownShrink @UnknownReplace Object unknown) { + @MaybeGrow @MaybeShrink @MaybeReplace Object unknown) { @Growable Object gv1 = gsr; @Growable Object gv2 = gs; @@ -147,19 +147,19 @@ void testCombinations( // :: error: [assignment] @Replaceable Object rv8 = unknown; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv1 = gsr; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv2 = gs; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv3 = gr; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv4 = sr; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv5 = g; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv6 = s; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv7 = r; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv8 = none; - @UnknownGrow @UnknownShrink @UnknownReplace Object tv9 = unknown; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv1 = gsr; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv2 = gs; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv3 = gr; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv4 = sr; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv5 = g; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv6 = s; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv7 = r; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv8 = none; + @MaybeGrow @MaybeShrink @MaybeReplace Object tv9 = unknown; } void testAliases( - @Modifiable Object mod, @Unmodifiable Object unmod, @UnknownModifiability Object unknown) { + @Modifiable Object mod, @Unmodifiable Object unmod, @MaybeModifiable Object unknown) { @Growable Object g1 = mod; @Shrinkable Object s1 = mod; @@ -169,12 +169,12 @@ void testAliases( @Unshrinkable Object us1 = unmod; @Unreplaceable Object ur1 = unmod; - @UnknownGrow Object ug2 = unknown; - @UnknownShrink Object us2 = unknown; - @UnknownReplace Object ur2 = unknown; + @MaybeGrow Object ug2 = unknown; + @MaybeShrink Object us2 = unknown; + @MaybeReplace Object ur2 = unknown; - @UnknownModifiability Object unknown1 = mod; - @UnknownModifiability Object unknown2 = unmod; + @MaybeModifiable Object unknown1 = mod; + @MaybeModifiable Object unknown2 = unmod; // :: error: [assignment] @Unmodifiable Object unmod1 = mod; diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 8b1592beca54..6d52734c7f8d 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -69,7 +69,7 @@ void copyOnWriteArrayListIterator() { CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); @Unshrinkable Iterator iterator = list.iterator(); - // store CopyOnWriteArrayList as a list, the iterator is @UnknownShrink. + // store CopyOnWriteArrayList as a list, the iterator is @MaybeShrink. List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), diff --git a/checker/tests/modifiability/MapModifiableTest.java b/checker/tests/modifiability/MapModifiableTest.java index b48493b56289..8a83eb044b30 100644 --- a/checker/tests/modifiability/MapModifiableTest.java +++ b/checker/tests/modifiability/MapModifiableTest.java @@ -3,8 +3,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; // Tests for Map mutating and read-only methods with modifiability qualifiers. @@ -95,7 +95,7 @@ void testEntryModifiabilityLocals(@Modifiable Entry modEntry) { } void testAnnotatedLocalsFromParams( - @UnknownModifiability Map any, + @MaybeModifiable Map any, @Modifiable Map modSrc, @Unmodifiable Map unmodSrc) { diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 473d2e4ffcab..d11b9dd246b6 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,10 +1,10 @@ import java.util.List; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownModifiability; import org.checkerframework.checker.modifiability.qual.Unmodifiable; /** @@ -20,85 +20,85 @@ public class PolyModifiableTest { void testPoly( @Modifiable List mod, - @Growable @Shrinkable List gs, // Grow+Shrink; Replace = UnknownReplace (default) - @Growable @Replaceable List gr, // Grow+Replace; Shrink = UnknownShrink - @Shrinkable @Replaceable List sr, // Shrink+Replace; Grow = UnknownGrow + @Growable @Shrinkable List gs, // Grow+Shrink; Replace = MaybeReplace (default) + @Growable @Replaceable List gr, // Grow+Replace; Shrink = MaybeShrink + @Shrinkable @Replaceable List sr, // Shrink+Replace; Grow = MaybeGrow @Growable List g, @Shrinkable List s, @Replaceable List r, - @UnknownModifiability List unknown, + @MaybeModifiable List unknown, @Unmodifiable List unmod) { // ============================================================ // Identity on @Modifiable (G+S+R) // ============================================================ @Modifiable List m1 = identity(mod); // OK: poly resolves to G+S+R - @UnknownModifiability List m2 = identity(mod); // OK: G+S+R <: top in all + @MaybeModifiable List m2 = identity(mod); // OK: G+S+R <: top in all // ============================================================ - // Identity on @Growable @Shrinkable (G+S, R=UnknownReplace) + // Identity on @Growable @Shrinkable (G+S, R=MaybeReplace) // ============================================================ @Growable @Shrinkable List gs1 = identity(gs); // OK // :: error: [assignment] - @Modifiable List gs2 = identity(gs); // Error: R=Unknown !<: Replaceable - @Growable List gs3 = identity(gs); // OK: G+S+UnknownR <: G - @Shrinkable List gs4 = identity(gs); // OK: G+S+UnknownR <: S + @Modifiable List gs2 = identity(gs); // Error: R=Maybe !<: Replaceable + @Growable List gs3 = identity(gs); // OK: G+S+MaybeR <: G + @Shrinkable List gs4 = identity(gs); // OK: G+S+MaybeR <: S // :: error: [assignment] - @Replaceable List gs5 = identity(gs); // Error: UnknownR !<: Replaceable + @Replaceable List gs5 = identity(gs); // Error: MaybeR !<: Replaceable // ============================================================ - // Identity on @Growable @Replaceable (G+R, S=UnknownShrink) + // Identity on @Growable @Replaceable (G+R, S=MaybeShrink) // ============================================================ @Growable @Replaceable List gr1 = identity(gr); // OK // :: error: [assignment] - @Modifiable List gr2 = identity(gr); // Error: S=Unknown !<: Shrinkable + @Modifiable List gr2 = identity(gr); // Error: S=Maybe !<: Shrinkable @Growable List gr3 = identity(gr); // OK // :: error: [assignment] - @Shrinkable List gr4 = identity(gr); // Error: S=Unknown + @Shrinkable List gr4 = identity(gr); // Error: S=Maybe @Replaceable List gr5 = identity(gr); // OK // ============================================================ - // Identity on @Shrinkable @Replaceable (S+R, G=UnknownGrow) + // Identity on @Shrinkable @Replaceable (S+R, G=MaybeGrow) // ============================================================ @Shrinkable @Replaceable List sr1 = identity(sr); // OK // :: error: [assignment] - @Modifiable List sr2 = identity(sr); // Error: G=Unknown !<: Growable + @Modifiable List sr2 = identity(sr); // Error: G=Maybe !<: Growable // :: error: [assignment] - @Growable List sr3 = identity(sr); // Error: G=Unknown + @Growable List sr3 = identity(sr); // Error: G=Maybe @Shrinkable List sr4 = identity(sr); // OK @Replaceable List sr5 = identity(sr); // OK // ============================================================ - // Identity on @Growable (G only; S=Unknown, R=Unknown) + // Identity on @Growable (G only; S=Maybe, R=Maybe) // ============================================================ @Growable List g1 = identity(g); // OK // :: error: [assignment] - @Growable @Shrinkable List g2 = identity(g); // Error: S=Unknown + @Growable @Shrinkable List g2 = identity(g); // Error: S=Maybe // :: error: [assignment] - @Modifiable List g3 = identity(g); // Error: S=Unknown, R=Unknown + @Modifiable List g3 = identity(g); // Error: S=Maybe, R=Maybe // ============================================================ - // Identity on @Shrinkable (S only; G=Unknown, R=Unknown) + // Identity on @Shrinkable (S only; G=Maybe, R=Maybe) // ============================================================ @Shrinkable List s1 = identity(s); // OK // :: error: [assignment] - @Growable @Shrinkable List s2 = identity(s); // Error: G=Unknown + @Growable @Shrinkable List s2 = identity(s); // Error: G=Maybe // :: error: [assignment] @Modifiable List s3 = identity(s); // Error // ============================================================ - // Identity on @Replaceable (R only; G=Unknown, S=Unknown) + // Identity on @Replaceable (R only; G=Maybe, S=Maybe) // ============================================================ @Replaceable List r1 = identity(r); // OK // :: error: [assignment] - @Growable @Replaceable List r2 = identity(r); // Error: G=Unknown + @Growable @Replaceable List r2 = identity(r); // Error: G=Maybe // :: error: [assignment] @Modifiable List r3 = identity(r); // Error // ============================================================ - // Identity on @UnknownModifiability (all tops) + // Identity on @MaybeModifiable (all tops) // ============================================================ - @UnknownModifiability List u1 = identity(unknown); // OK + @MaybeModifiable List u1 = identity(unknown); // OK // :: error: [assignment] @Modifiable List u2 = identity(unknown); // Error // :: error: [assignment] @@ -108,7 +108,7 @@ void testPoly( // Identity on @Unmodifiable (Ungrowable+Unshrinkable+Unreplaceable) // ============================================================ @Unmodifiable List unmod1 = identity(unmod); // OK - @UnknownModifiability List unmod2 = identity(unmod); // OK + @MaybeModifiable List unmod2 = identity(unmod); // OK // :: error: [assignment] @Modifiable List unmod3 = identity(unmod); // Error } diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 5177ee8d6381..6b183306d58e 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -44,7 +44,7 @@ public SetRefinementTest(List other, float dummy) { this.items.addAll(other); } - // Negative field case: in a regular method, items has its declared type @UnknownModifiability. + // Negative field case: in a regular method, items has its declared type @MaybeModifiable. public void updateItemsLikeConstructor(List other, int dummy) { // :: error: [method.invocation] this.items.set(0, other.get(0)); @@ -97,7 +97,7 @@ public void arrayAccessReceiverSideEffect(List other) { arr[0] = new ArrayList<>(); // arr[0] is @Modifiable arr[0].add("x"); // side effect without @DoesNotUnrefineReceiver; refinement of arr[0] cleared // :: error: [method.invocation] - arr[0].addAll(other); // ERROR: arr[0] is @UnknownModifiability (declared type) + arr[0].addAll(other); // ERROR: arr[0] is @MaybeModifiable (declared type) } // ========================================================== diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 3485081cb076..762a5dffb3c5 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -3,20 +3,19 @@ import java.util.Queue; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeGrow; +import org.checkerframework.checker.modifiability.qual.MaybeReplace; +import org.checkerframework.checker.modifiability.qual.MaybeShrink; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnknownGrow; -import org.checkerframework.checker.modifiability.qual.UnknownReplace; -import org.checkerframework.checker.modifiability.qual.UnknownShrink; /** * Tests structural capability removal for Set, Queue, Map.Entry, and Iterator. * - *

Convenience aliases on Set / Queue lose the Replace capability (set Replace - * to @UnknownReplace). Convenience aliases on Map.Entry lose Grow and Shrink capabilities. - * Convenience aliases on Iterator lose Grow and Replace capabilities. Explicit capability - * annotations are preserved. + *

Convenience aliases on Set / Queue lose the Replace capability (set Replace to @MaybeReplace). + * Convenience aliases on Map.Entry lose Grow and Shrink capabilities. Convenience aliases on + * Iterator lose Grow and Replace capabilities. Explicit capability annotations are preserved. */ class SetTest { @@ -35,26 +34,26 @@ void testSetReplaceRemoved( @Growable @Shrinkable Set gs1 = modifiable; // OK // :: error: [assignment] - @Replaceable Set r2 = modifiable; // unknownReplace cannot assign to replaceable. + @Replaceable Set r2 = modifiable; // maybeReplace cannot assign to replaceable. } void testSetGrowShrinkPreserved( - @UnknownGrow @UnknownShrink @UnknownReplace Set unknown, + @MaybeGrow @MaybeShrink @MaybeReplace Set unknown, @Growable Set growable, @Shrinkable Set shrinkable, @Growable @Shrinkable Set gs) { // Top in all hierarchies: accepts everything - @UnknownGrow @UnknownShrink @UnknownReplace Set u1 = growable; - @UnknownGrow @UnknownShrink @UnknownReplace Set u2 = shrinkable; - @UnknownGrow @UnknownShrink @UnknownReplace Set u3 = gs; + @MaybeGrow @MaybeShrink @MaybeReplace Set u1 = growable; + @MaybeGrow @MaybeShrink @MaybeReplace Set u2 = shrinkable; + @MaybeGrow @MaybeShrink @MaybeReplace Set u3 = gs; - // @Growable Set: Grow=G, Shrink=Unknown, Replace=Unknown + // @Growable Set: Grow=G, Shrink=Maybe, Replace=Maybe @Growable Set g1 = gs; // OK: G+S <: G // :: error: [assignment] @Growable Set g2 = shrinkable; // Error: S only, no G - // @Shrinkable Set: Grow=Unknown, Shrink=S, Replace=Unknown + // @Shrinkable Set: Grow=Maybe, Shrink=S, Replace=Maybe @Shrinkable Set s1 = gs; // OK: G+S <: S // :: error: [assignment] @Shrinkable Set s2 = growable; // Error: G only, no S @@ -76,8 +75,8 @@ void testQueueLikeSet( // ========================================================== void testEntryAssignment( - // No Replace bit: effective Replace = @UnknownReplace after G+S removal - Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry unknown, + // No Replace bit: effective Replace = @MaybeReplace after G+S removal + Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry unknown, Map.@Growable Entry growable, Map.@Shrinkable Entry shrinkable, Map.@Growable @Shrinkable Entry gs, @@ -88,11 +87,11 @@ void testEntryAssignment( Map.@Modifiable Entry modifiable, Map.@Growable @Shrinkable @Replaceable Entry explicit) { - // Group 1: entries without Replace bit become @UnknownGrow @UnknownShrink @UnknownReplace - // (G and S are removed → both become Unknown; R was already Unknown) - Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u1 = growable; - Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u2 = shrinkable; - Map.@UnknownGrow @UnknownShrink @UnknownReplace Entry u3 = gs; + // Group 1: entries without Replace bit become @MaybeGrow @MaybeShrink @MaybeReplace + // (G and S are removed → both become Maybe; R was already Maybe) + Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry u1 = growable; + Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry u2 = shrinkable; + Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry u3 = gs; // Explicit Grow/Shrink annotations are preserved. // :: error: [assignment] Map.@Growable Entry g1 = unknown; @@ -100,9 +99,9 @@ void testEntryAssignment( Map.@Shrinkable Entry s1 = unknown; Map.@Growable Entry g2 = gs; // OK - Map.@UnknownGrow @UnknownShrink @Replaceable Entry r1 = growReplace; - Map.@UnknownGrow @UnknownShrink @Replaceable Entry r2 = shrinkReplace; - Map.@UnknownGrow @UnknownShrink @Replaceable Entry r3 = modifiable; + Map.@MaybeGrow @MaybeShrink @Replaceable Entry r1 = growReplace; + Map.@MaybeGrow @MaybeShrink @Replaceable Entry r2 = shrinkReplace; + Map.@MaybeGrow @MaybeShrink @Replaceable Entry r3 = modifiable; // Explicit Grow/Shrink annotations are preserved on Map.Entry. // :: error: [assignment] Map.@Growable @Replaceable Entry gr1 = replaceable; // OK: same effective type @@ -114,10 +113,10 @@ void testEntryAssignment( Map.@Replaceable Entry gr2 = modifiable; Map.@Modifiable Entry m2 = explicit; - // Cross-group: Replace !<: UnknownReplace (wrong direction) is NOT an error — - // but UnknownReplace !<: Replaceable IS an error: + // Cross-group: Replace !<: MaybeReplace (wrong direction) is NOT an error — + // but MaybeReplace !<: Replaceable IS an error: // :: error: [assignment] - Map.@Replaceable Entry bad1 = growable; // Error: R=Unknown !<: Replaceable + Map.@Replaceable Entry bad1 = growable; // Error: R=Maybe !<: Replaceable } // ========================================================== @@ -126,22 +125,22 @@ void testEntryAssignment( // ========================================================== void testIteratorAssignment( - // No Shrink bit: effective after G+R removal → @UnknownGrow @UnknownShrink @UnknownReplace - @UnknownGrow @UnknownShrink @UnknownReplace Iterator unknown, + // No Shrink bit: effective after G+R removal → @MaybeGrow @MaybeShrink @MaybeReplace + @MaybeGrow @MaybeShrink @MaybeReplace Iterator unknown, @Growable Iterator growable, @Replaceable Iterator replaceable, @Growable @Replaceable Iterator growReplace, - // Has Shrink bit: effective after G+R removal → @UnknownGrow @Shrinkable @UnknownReplace + // Has Shrink bit: effective after G+R removal → @MaybeGrow @Shrinkable @MaybeReplace @Shrinkable Iterator shrinkable, @Growable @Shrinkable Iterator growShrink, @Shrinkable @Replaceable Iterator shrinkReplace, @Modifiable Iterator modifiable, @Growable @Shrinkable @Replaceable Iterator explicit) { - // Group 1: No Shrink bit → effective @UnknownGrow @UnknownShrink @UnknownReplace - @UnknownGrow @UnknownShrink @UnknownReplace Iterator u1 = growable; - @UnknownGrow @UnknownShrink @UnknownReplace Iterator u2 = replaceable; - @UnknownGrow @UnknownShrink @UnknownReplace Iterator u3 = growReplace; + // Group 1: No Shrink bit → effective @MaybeGrow @MaybeShrink @MaybeReplace + @MaybeGrow @MaybeShrink @MaybeReplace Iterator u1 = growable; + @MaybeGrow @MaybeShrink @MaybeReplace Iterator u2 = replaceable; + @MaybeGrow @MaybeShrink @MaybeReplace Iterator u3 = growReplace; // Explicit Grow/Replace annotations are preserved. // :: error: [assignment] @Growable Iterator g1 = unknown; @@ -149,11 +148,11 @@ void testIteratorAssignment( // :: error: [assignment] @Growable @Replaceable Iterator gr1 = replaceable; - // Group 2: Has Shrink bit → effective @UnknownGrow @Shrinkable @UnknownReplace - @UnknownGrow @Shrinkable @UnknownReplace Iterator s1 = shrinkable; - @UnknownGrow @Shrinkable @UnknownReplace Iterator s2 = growShrink; - @UnknownGrow @Shrinkable @UnknownReplace Iterator s3 = shrinkReplace; - @UnknownGrow @Shrinkable @UnknownReplace Iterator s4 = modifiable; + // Group 2: Has Shrink bit → effective @MaybeGrow @Shrinkable @MaybeReplace + @MaybeGrow @Shrinkable @MaybeReplace Iterator s1 = shrinkable; + @MaybeGrow @Shrinkable @MaybeReplace Iterator s2 = growShrink; + @MaybeGrow @Shrinkable @MaybeReplace Iterator s3 = shrinkReplace; + @MaybeGrow @Shrinkable @MaybeReplace Iterator s4 = modifiable; // Cross-assignments within group 2: @Shrinkable Iterator sh1 = modifiable; // :: error: [assignment] @@ -162,6 +161,6 @@ void testIteratorAssignment( @Shrinkable @Replaceable Iterator sr1 = modifiable; @Modifiable Iterator m1 = explicit; // :: error: [assignment] - @Shrinkable Iterator bad1 = growable; // Error: Shrink=Unknown !<: Shrinkable + @Shrinkable Iterator bad1 = growable; // Error: Shrink=Maybe !<: Shrinkable } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 515aa6287ad0..ec472b807af2 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -85,7 +85,7 @@ \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @UnknownGrow @UnknownShrink @UnknownReplace @UnknownIteratorPolyMod + @MaybeGrow @MaybeShrink @MaybeReplace @MaybeIteratorPolyMod / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | @@ -101,10 +101,10 @@ Figure~\ref{fig-modifiability-lattice} shows the hierarchies. An assignment \code{T x = y} is valid only if the compile-time type of \code{y} is a subtype of (or equal to) \code{T} in every hierarchy simultaneously. For -example, \code{@Growable @UnknownShrink @UnknownReplace List} is a -subtype of \code{@UnknownGrow @UnknownShrink @UnknownReplace List} -(because \<@Growable> $\le$ \<@UnknownGrow>), but it is \emph{not} a -subtype of \code{@Ungrowable @UnknownShrink @UnknownReplace List} +example, \code{@Growable @MaybeShrink @MaybeReplace List} is a +subtype of \code{@MaybeGrow @MaybeShrink @MaybeReplace List} +(because \<@Growable> $\le$ \<@MaybeGrow>), but it is \emph{not} a +subtype of \code{@Ungrowable @MaybeShrink @MaybeReplace List} (because \<@Growable> and \<@Ungrowable> are incomparable siblings in the Grow hierarchy). @@ -129,8 +129,8 @@ always throws \UnsupportedOperationException. The checker issues a warning at every invocation of a grow, shrink, or replace operation. -\item[\refqualclass{checker/modifiability/qual}{UnknownModifiability}] -Alias for \code{@UnknownGrow @UnknownShrink @UnknownReplace}. +\item[\refqualclass{checker/modifiability/qual}{MaybeModifiable}] +Alias for \code{@MaybeGrow @MaybeShrink @MaybeReplace}. The checker cannot determine the modifiability of the collection. The checker conservatively issues a warning at every invocation of a grow, shrink, or replace operation. @@ -147,14 +147,14 @@ \begin{Verbatim} @Modifiable List mod = new ArrayList<>(); // all three capabilities @Unmodifiable List unmod = List.of("a"); // no capabilities - @UnknownModifiability List unknown = ...; // capabilities are unknown at compile time + @MaybeModifiable List unknown = ...; // capabilities are unknown at compile time \end{Verbatim} \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} Qualifiers in the Grow hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{UnknownGrow}] +\item[\refqualclass{checker/modifiability/qual}{MaybeGrow}] The top qualifier in the Grow hierarchy. The checker cannot determine whether this collection supports grow operations. This is the default qualifier for unannotated types in the @@ -174,7 +174,7 @@ Qualifiers in the Shrink hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{UnknownShrink}] +\item[\refqualclass{checker/modifiability/qual}{MaybeShrink}] The top qualifier in the Shrink hierarchy. The checker cannot determine whether this collection supports shrink operations. This is the default qualifier for unannotated types in the @@ -195,7 +195,7 @@ Qualifiers in the Replace hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{UnknownReplace}] +\item[\refqualclass{checker/modifiability/qual}{MaybeReplace}] The top qualifier in the Replace hierarchy. The checker cannot determine whether this collection supports replace operations. This is the default qualifier for unannotated types in the @@ -216,7 +216,7 @@ Qualifiers in the Iterator hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{UnknownIteratorPolyMod}] +\item[\refqualclass{checker/modifiability/qual}{MaybeIteratorPolyMod}] The top qualifier. The checker cannot determine whether this collection's \code{iterator()} is \<@PolyShrink>. @@ -234,16 +234,16 @@ To express multiple capabilities, write one annotation per hierarchy: \begin{Verbatim} - @Growable @Shrinkable List // can grow and shrink, replace unknown - @Growable @Replaceable List // can grow and replace, shrink unknown + @Growable @Shrinkable List // can grow and shrink, maybe replace + @Growable @Replaceable List // can grow and replace, maybe shrink @Growable @Shrinkable @Replaceable List // all three capabilities \end{Verbatim} Because the three hierarchies are independent, subtyping checks each hierarchy individually. A \code{@Growable @Shrinkable List} is a -subtype of \code{@Growable List} (Shrink defaults to \<@UnknownGrow> on the +subtype of \code{@Growable List} (Shrink defaults to \<@MaybeShrink> on the right, Replace -defaults to \<@UnknownReplace> on both sides), but it is \emph{not} a +defaults to \<@MaybeReplace> on both sides), but it is \emph{not} a subtype of \code{@Growable @Shrinkable @Replaceable List} (due to the Replace qualifiers). @@ -290,13 +290,13 @@ If the map is \code{@Growable @Shrinkable @Replaceable}, then \code{keySet} returns -\code{@Ungrowable @Shrinkable @UnknownReplace Set}. +\code{@Ungrowable @Shrinkable @MaybeReplace Set}. -If the map is \code{@UnknownGrow @UnknownShrink @UnknownReplace}, then -\code{keySet} returns \code{@Ungrowable @UnknownShrink @UnknownReplace Set}. +If the map is \code{@MaybeGrow @MaybeShrink @MaybeReplace}, then +\code{keySet} returns \code{@Ungrowable @MaybeShrink @MaybeReplace Set}. If the map is \code{@Ungrowable @Unshrinkable @Unreplaceable}, then -\code{keySet} returns \code{@Ungrowable @Unshrinkable @UnknownReplace Set}. +\code{keySet} returns \code{@Ungrowable @Unshrinkable @MaybeReplace Set}. \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} @@ -326,7 +326,7 @@ When a programmer writes \<@Modifiable> or \<@Unmodifiable> on a type that has no method for a given mutation, the Modifiability Checker uses the top qualifier. For example, both \<@Modifiable Queue> and \<@Unmodifiable -Queue> have \<@UnknownReplace> as their replaceability qualifier. (In the +Queue> have \<@MaybeReplace> as their replaceability qualifier. (In the above assignment, \ was flow-sensitively refined to have type \<@Replaceable>.) Other examples are that \ has no replace methods and \ has no grow or replace methods. @@ -359,7 +359,7 @@ A collection uses the second signature if the collection is \<@IteratorPolyMod>. The collection uses the first signature if the -collection is \<@UnknownIteratorPolyMod>. +collection is \<@MaybeIteratorPolyMod>. Here are examples of its use: @@ -418,7 +418,7 @@ map.put("key", "value"); // OK // Map.remove requires Shrink capability - map.remove("key"); // Error: @UnknownShrink (default) !<: @Shrinkable + map.remove("key"); // Error: @MaybeShrink (default) !<: @Shrinkable } // @PolyModifiable preserves all three capabilities @@ -430,7 +430,7 @@ @Modifiable List m = wrap(mod); // OK @Growable @Shrinkable List gs2 = wrap(gs); // OK // :: error: [assignment] - @Modifiable List bad = wrap(gs); // Error: gs has @UnknownReplace + @Modifiable List bad = wrap(gs); // Error: gs has @MaybeReplace } } \end{Verbatim} @@ -438,10 +438,10 @@ % LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap % LocalWords: toArray addAll removeAll retainAll replaceAll setValue -% LocalWords: UnknownModifiability Growable UnknownGrow UnknownShrink -% LocalWords: UnknownReplace growable ListIterator PolyModifiable addFirst +% LocalWords: MaybeModifiable Growable MaybeGrow MaybeShrink +% LocalWords: MaybeReplace growable ListIterator PolyModifiable addFirst % LocalWords: synchronizedList PolyShrink PolyGrow PolyReplace ThrowsUOE % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace -% LocalWords: UnknownIteratorPolyMod IteratorPolyMod +% LocalWords: MaybeIteratorPolyMod IteratorPolyMod From 0589bff0b7469bae4cca9e988e042059e965c962 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 18 May 2026 15:28:58 -0700 Subject: [PATCH 170/308] add unmodParam annotation Co-authored-by: Copilot --- buildSrc/src/main/groovy/cf-spotless.gradle | 1 + .../modifiability/qual/UnmodParam.java | 23 +++++++++++++++++++ .../grow/GrowAnnotatedTypeFactory.java | 4 +++- .../replace/ReplaceAnnotatedTypeFactory.java | 4 +++- .../shrink/ShrinkAnnotatedTypeFactory.java | 4 +++- .../modifiability/BasicModifiabilityTest.java | 10 ++++++++ docs/manual/modifiability-checker.tex | 7 +++++- 7 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 65ea1476bea7..5c375969a4a8 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -111,6 +111,7 @@ spotless { .addTypeAnnotation("MaybeReplace") .addTypeAnnotation("MaybeShrink") .addTypeAnnotation("Unmodifiable") + .addTypeAnnotation("UnmodParam") .addTypeAnnotation("Unreplaceable") .addTypeAnnotation("Unshrinkable") } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java new file mode 100644 index 000000000000..d11d7e0f30eb --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Convenience alias meaning {@code @MaybeGrow @MaybeShrink @MaybeReplace}. This is a syntactic + * sugar for {@link MaybeModifiable}. It is intended to be used on parameters of methods that are + * not designed to change the parameter. This annotation is only allowed to be written on formal + * parameters. + * + *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to + * {@code @MaybeGrow @MaybeShrink @MaybeReplace} on each annotated type. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface UnmodParam {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 79bc52145978..55790b56e58f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -22,6 +22,7 @@ import org.checkerframework.checker.modifiability.qual.PolyGrow; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Ungrowable; +import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -123,7 +124,8 @@ public AnnotationMirror canonicalAnnotation( return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : GROWABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : UNGROWABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class)) { + } else if (areSameByClass(annotation, MaybeModifiable.class) + || areSameByClass(annotation, UnmodParam.class)) { return MAYBE_GROW; } else if (areSameByClass(annotation, PolyModifiable.class)) { return tm != null && isMapEntry(tm) ? MAYBE_GROW : POLY_GROW; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 638f24df258e..04a4672e2a4f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -21,6 +21,7 @@ import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplace; import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.nullness.qual.Nullable; @@ -138,7 +139,8 @@ public AnnotationMirror canonicalAnnotation( return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : REPLACEABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : UNREPLACEABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class)) { + } else if (areSameByClass(annotation, MaybeModifiable.class) + || areSameByClass(annotation, UnmodParam.class)) { return MAYBE_REPLACE; } else if (areSameByClass(annotation, PolyModifiable.class)) { return POLY_REPLACE; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index f28c0bd9670f..721dd3d885fd 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -21,6 +21,7 @@ import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrink; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.checker.nullness.qual.Nullable; @@ -119,7 +120,8 @@ public AnnotationMirror canonicalAnnotation( return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : SHRINKABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : UNSHRINKABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class)) { + } else if (areSameByClass(annotation, MaybeModifiable.class) + || areSameByClass(annotation, UnmodParam.class)) { return MAYBE_SHRINK; } else if (areSameByClass(annotation, PolyModifiable.class)) { return tm != null && isMapEntry(tm) ? MAYBE_SHRINK : POLY_SHRINK; diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 175ad8461d6a..269ed6241c2e 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -4,6 +4,7 @@ import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; // this checkes the correctness of annotation in List.java and ONLY the initialization of ArrayList @@ -101,6 +102,15 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List unmod.retainAll(other); } + void testUnmodParamMutation(@UnmodParam List unmodParam, String value) { + // :: error: [method.invocation] + unmodParam.add(value); + // :: error: [method.invocation] + unmodParam.remove(value); + // :: error: [method.invocation] + unmodParam.set(0, value); + } + void testReplaceAll( @IteratorPolyMod @Modifiable List mod, @Unmodifiable List unmod) { mod.replaceAll(s -> s + "!"); diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index ec472b807af2..de1ea83fa4c1 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -135,6 +135,10 @@ The checker conservatively issues a warning at every invocation of a grow, shrink, or replace operation. +\item[\refqualclass{checker/modifiability/qual}{UnmodParam}] +A syntactic sugar for \refqualclass{checker/modifiability/qual}{MaybeModifiable}. +Could only be written on formal parameters when the method is not designed to change the parameter. + \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrow @PolyShrink @PolyReplace} (see Section~\ref{modifiability-polymorphic}). @@ -148,6 +152,7 @@ @Modifiable List mod = new ArrayList<>(); // all three capabilities @Unmodifiable List unmod = List.of("a"); // no capabilities @MaybeModifiable List unknown = ...; // capabilities are unknown at compile time + void foo(@UnmodParam List list) { ... } // foo does not modify list \end{Verbatim} \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} @@ -438,7 +443,7 @@ % LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap % LocalWords: toArray addAll removeAll retainAll replaceAll setValue -% LocalWords: MaybeModifiable Growable MaybeGrow MaybeShrink +% LocalWords: MaybeModifiable UnmodParam Growable MaybeGrow MaybeShrink % LocalWords: MaybeReplace growable ListIterator PolyModifiable addFirst % LocalWords: synchronizedList PolyShrink PolyGrow PolyReplace ThrowsUOE % LocalWords: addLast unreplaceable Growability shrinkability replacability From f9f9a8ee5d92c340d36fc0d5d26eccb46a15d819 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 19 May 2026 16:10:28 -0700 Subject: [PATCH 171/308] setRefinementTest is fixed in jdk. --- checker/tests/modifiability/SetRefinementTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 6b183306d58e..7274822918bc 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -25,22 +25,18 @@ public SetRefinementTest(List other) { public SetRefinementTest(List other, int dummy) { this.items.set(0, other.get(0)); - // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, boolean dummy) { this.items.set(0, other.get(0)); - // :: error: [method.invocation] this.items.set(0, other.get(0)); - // :: error: [method.invocation] this.items.addAll(other); } public SetRefinementTest(List other, float dummy) { this.items.get(0); this.items.set(0, other.get(0)); - // :: error: [method.invocation] this.items.addAll(other); } @@ -86,7 +82,6 @@ public void arrayAccessReceiver(List other) { List[] arr = new ArrayList[1]; arr[0] = new ArrayList<>(); arr[0].set(0, other.get(0)); - // :: error: [method.invocation] arr[0].addAll(other); } @@ -96,7 +91,6 @@ public void arrayAccessReceiverSideEffect(List other) { List[] arr = new ArrayList[1]; arr[0] = new ArrayList<>(); // arr[0] is @Modifiable arr[0].add("x"); // side effect without @DoesNotUnrefineReceiver; refinement of arr[0] cleared - // :: error: [method.invocation] arr[0].addAll(other); // ERROR: arr[0] is @MaybeModifiable (declared type) } From d59f5aa35bdb7226f26429475cb5eaac0b76908a Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 19 May 2026 16:53:46 -0700 Subject: [PATCH 172/308] fix setRefinementTest. --- checker/tests/modifiability/SetRefinementTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 7274822918bc..344eaa5a1588 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -85,13 +85,15 @@ public void arrayAccessReceiver(List other) { arr[0].addAll(other); } - // Negative: add() lacks @DoesNotUnrefineReceiver, so the @Modifiable refinement is lost. + // Positive: add() also preserves the @Modifiable refinement of the array element. + // Collection.add() is annotated @DoesNotUnrefineReceiver("modifiability"), and that declaration + // annotation is inherited by overriding methods such as List.add(). @SuppressWarnings("unchecked") public void arrayAccessReceiverSideEffect(List other) { List[] arr = new ArrayList[1]; arr[0] = new ArrayList<>(); // arr[0] is @Modifiable - arr[0].add("x"); // side effect without @DoesNotUnrefineReceiver; refinement of arr[0] cleared - arr[0].addAll(other); // ERROR: arr[0] is @MaybeModifiable (declared type) + arr[0].add("x"); // side effect with @DoesNotUnrefineReceiver; refinement of arr[0] preserved + arr[0].addAll(other); } // ========================================================== From f30d590adf370404e790615d3ba68defd226d49a Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 19 May 2026 18:29:42 -0700 Subject: [PATCH 173/308] forbid unmodparam on non-parameters. Co-authored-by: Copilot --- .../modifiability/qual/UnmodParam.java | 4 +- .../modifiability/ModifiabilityVisitor.java | 80 +++++++++++++++++++ .../modifiability/replace/ReplaceVisitor.java | 6 ++ .../modifiability/shrink/ShrinkVisitor.java | 6 ++ .../checker/modifiability/messages.properties | 1 + .../modifiability/UnmodParamLocationTest.java | 47 +++++++++++ docs/manual/modifiability-checker.tex | 5 +- 7 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 checker/tests/modifiability/UnmodParamLocationTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java index d11d7e0f30eb..e065abdb7a5f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java @@ -9,8 +9,8 @@ /** * Convenience alias meaning {@code @MaybeGrow @MaybeShrink @MaybeReplace}. This is a syntactic * sugar for {@link MaybeModifiable}. It is intended to be used on parameters of methods that are - * not designed to change the parameter. This annotation is only allowed to be written on formal - * parameters. + * not designed to change the parameter. This annotation is only allowed to be written within method + * or constructor parameter types, or explicit receiver parameter types. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to * {@code @MaybeGrow @MaybeShrink @MaybeReplace} on each annotated type. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index ce7e0432705a..ae879762287f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -1,8 +1,15 @@ package org.checkerframework.checker.modifiability; +import com.sun.source.tree.AnnotationTree; 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 javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import org.checkerframework.checker.modifiability.qual.ThrowsUOE; +import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -31,6 +38,16 @@ public ModifiabilityVisitor(BaseTypeChecker checker) { super(checker); } + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + if (shouldCheckUnmodParamLocation() && isUnmodParamAnnotation(tree)) { + if (!isWithinAllowedUnmodParamLocation()) { + checker.reportError(tree, "unmodparam.location"); + } + } + return super.visitAnnotation(tree, p); + } + @Override public Void visitMethodInvocation(MethodInvocationTree node, Void p) { // Methods annotated @ThrowsUOE always throw UnsupportedOperationException when called. @@ -61,6 +78,69 @@ protected boolean shouldCheckThrowsUOE() { return true; } + /** + * Returns true if this checker should report {@code @UnmodParam} location errors. + * + *

The default is {@code true}. Shrink and Replace override this to avoid tripling diagnostics + * when running under the aggregate {@link ModifiabilityChecker}. + * + * @return true if this visitor should report {@code @UnmodParam} location errors + */ + protected boolean shouldCheckUnmodParamLocation() { + return true; + } + + /** + * Returns true if {@code tree} is an {@link UnmodParam} annotation. + * + * @param tree an annotation tree + * @return true if {@code tree} is an {@code @UnmodParam} annotation + */ + private boolean isUnmodParamAnnotation(AnnotationTree tree) { + String annotationName = tree.getAnnotationType().toString(); + // Quick check to avoid expensive annotation resolution for most annotations. + if (!annotationName.equals("UnmodParam") && !annotationName.endsWith(".UnmodParam")) { + return false; + } + + AnnotationMirror annotation = TreeUtils.annotationFromAnnotationTree(tree); + return annotation != null && atypeFactory.areSameByClass(annotation, UnmodParam.class); + } + + /** + * Returns true if the current annotation path is inside a method/constructor parameter type or + * explicit receiver parameter type. + * + * @return true if {@code @UnmodParam} is allowed at the current location + */ + private boolean isWithinAllowedUnmodParamLocation() { + // Find the declaration that contains the annotation, if any. + TreePath path = getCurrentPath(); + TreePath variablePath = null; + while (path != null) { + if (path.getLeaf() instanceof VariableTree) { + variablePath = path; + break; + } + path = path.getParentPath(); + } + + if (variablePath == null || variablePath.getParentPath() == null) { + return false; + } + + // Ordinary and receiver parameters are represented as VariableTrees under a MethodTree. + Tree parent = variablePath.getParentPath().getLeaf(); + if (!(parent instanceof MethodTree methodTree)) { + return false; + } + + // Allow nested annotations anywhere within an allowed parameter's type. + VariableTree variable = (VariableTree) variablePath.getLeaf(); + return methodTree.getParameters().contains(variable) + || methodTree.getReceiverParameter() == variable; + } + // Suppresses the framework's "constructor result must be TOP" check. // Collection constructors (e.g., new ArrayList()) legitimately produce @Modifiable, which is a // subtype of the top type @MaybeModifiable. This suppression is sound: constructors are diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index f13c02c4a69c..5965ea651a90 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -20,4 +20,10 @@ protected boolean shouldCheckThrowsUOE() { // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. return checker.getParentChecker() == null; } + + @Override + protected boolean shouldCheckUnmodParamLocation() { + // When running under ModifiabilityChecker, GrowChecker handles @UnmodParam location reporting. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index 83000bc897ad..84e690f04b9f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -20,4 +20,10 @@ protected boolean shouldCheckThrowsUOE() { // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. return checker.getParentChecker() == null; } + + @Override + protected boolean shouldCheckUnmodParamLocation() { + // When running under ModifiabilityChecker, GrowChecker handles @UnmodParam location reporting. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index fdc062024160..d9669b0d2b64 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1 +1,2 @@ usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. +unmodparam.location=@UnmodParam may only be written on method/constructor parameters or explicit receiver parameters. diff --git a/checker/tests/modifiability/UnmodParamLocationTest.java b/checker/tests/modifiability/UnmodParamLocationTest.java new file mode 100644 index 000000000000..35a8fac47710 --- /dev/null +++ b/checker/tests/modifiability/UnmodParamLocationTest.java @@ -0,0 +1,47 @@ +import java.util.List; +import java.util.function.Consumer; +import org.checkerframework.checker.modifiability.qual.UnmodParam; + +class UnmodParamLocationTest< + // :: error: [unmodparam.location] + @UnmodParam T> { + + // :: error: [unmodparam.location] + @UnmodParam List field; + + // :: error: [unmodparam.location] + List<@UnmodParam List> nestedField; + + UnmodParamLocationTest(@UnmodParam List parameter) {} + + void method(@UnmodParam List parameter) {} + + void nestedParameter(List<@UnmodParam List> parameter) {} + + void receiver(@UnmodParam UnmodParamLocationTest this) {} + + // :: error: [unmodparam.location] + @UnmodParam List returnType() { + return null; + } + + @SuppressWarnings("unchecked") + void local(Object object) { + // :: error: [unmodparam.location] + @UnmodParam List local = null; + local = null; + + // :: error: [unmodparam.location] + List cast = (@UnmodParam List) object; + cast = null; + } + + void lambda() { + // :: error: [unmodparam.location] + Consumer> consumer = (@UnmodParam List parameter) -> {}; + consumer.accept(null); + } + + // :: error: [unmodparam.location] + <@UnmodParam S> void methodTypeParameter(S value) {} +} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index de1ea83fa4c1..d76519f72941 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -137,7 +137,10 @@ \item[\refqualclass{checker/modifiability/qual}{UnmodParam}] A syntactic sugar for \refqualclass{checker/modifiability/qual}{MaybeModifiable}. -Could only be written on formal parameters when the method is not designed to change the parameter. +It may only be written within a method or constructor parameter type, or within +an explicit receiver parameter type, when the method is not designed to change +the parameter. It may appear nested within such a parameter type, such as in +\code{List<@UnmodParam List>}. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrow @PolyShrink @PolyReplace} From 4bacd4b80dc1e24af9e905a75b51dbeace7a81ce Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 20 May 2026 21:51:12 -0700 Subject: [PATCH 174/308] add suppress interning:not.intered --- .../checker/modifiability/ModifiabilityVisitor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index ae879762287f..8c0058c29f52 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -113,6 +113,7 @@ private boolean isUnmodParamAnnotation(AnnotationTree tree) { * * @return true if {@code @UnmodParam} is allowed at the current location */ + @SuppressWarnings("interning:not.interned") // AST node comparison private boolean isWithinAllowedUnmodParamLocation() { // Find the declaration that contains the annotation, if any. TreePath path = getCurrentPath(); From 5594b5029919017d33231fe3b34cc77ce536bf31 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 21 May 2026 16:26:42 -0700 Subject: [PATCH 175/308] throw warning when user created their own collection class. Co-authored-by: Copilot --- .../modifiability/ModifiabilityVisitor.java | 66 +++++++++++++++++++ .../modifiability/replace/ReplaceVisitor.java | 6 ++ .../modifiability/shrink/ShrinkVisitor.java | 6 ++ .../checker/modifiability/messages.properties | 1 + .../CustomModifiabilityAnnotationWarning.java | 54 +++++++++++++++ .../modifiability/IteratorPrecisionTest.java | 5 ++ .../modifiability/SetRefinementTest.java | 1 + .../tests/all-systems/GenericsCasts.java | 1 + .../tests/all-systems/GenericsEnclosing.java | 1 + .../tests/all-systems/InferTypeArgs3.java | 1 + framework/tests/all-systems/Issue6810.java | 1 + .../tests/all-systems/RawTypeAssignment.java | 1 + .../all-systems/java8inference/Issue1715.java | 1 + .../all-systems/java8inference/Issue3036.java | 1 + .../java8inference/MemRefInfere.java | 1 + 15 files changed, 147 insertions(+) create mode 100644 checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 8c0058c29f52..aa8832b16363 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.modifiability; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -8,13 +9,17 @@ import com.sun.source.util.TreePath; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.qual.ThrowsUOE; import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** * Base visitor for the Modifiability sub-checkers (Grow, Shrink, Replace). @@ -29,6 +34,15 @@ */ public class ModifiabilityVisitor extends BaseTypeVisitor { + /** The erased {@code java.util.Collection} type. */ + private final TypeMirror collectionErasure; + + /** The erased {@code java.util.Map} type. */ + private final TypeMirror mapErasure; + + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; + /** * Create a ModifiabilityVisitor. * @@ -36,6 +50,27 @@ public class ModifiabilityVisitor extends BaseTypeVisitorThe default is {@code true}. Shrink and Replace override this to avoid tripling diagnostics + * when running under the aggregate {@link ModifiabilityChecker}. + * + * @return true if this visitor should report custom modifiability annotation warnings + */ + protected boolean shouldCheckCustomModifiabilityAnnotation() { + return true; + } + /** * Returns true if {@code tree} is an {@link UnmodParam} annotation. * @@ -150,4 +198,22 @@ private boolean isWithinAllowedUnmodParamLocation() { @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} + + /** + * Returns true if {@code typeElement} is a source-defined subtype of {@link + * java.util.Collection}, {@link java.util.Map}, or {@link java.util.Iterator}. + * + * @param typeElement the type element to test + * @return true if this source-defined type relies on trusted custom modifiability annotations + */ + private boolean isSourceDefinedModifiabilityType(TypeElement typeElement) { + if (!ElementUtils.isElementFromSourceCode(typeElement)) { + return false; + } + + TypeMirror type = typeElement.asType(); + return TypesUtils.isErasedSubtype(type, collectionErasure, atypeFactory.types) + || TypesUtils.isErasedSubtype(type, mapErasure, atypeFactory.types) + || TypesUtils.isErasedSubtype(type, iteratorErasure, atypeFactory.types); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 5965ea651a90..a18d5ec13ad3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -26,4 +26,10 @@ protected boolean shouldCheckUnmodParamLocation() { // When running under ModifiabilityChecker, GrowChecker handles @UnmodParam location reporting. return checker.getParentChecker() == null; } + + @Override + protected boolean shouldCheckCustomModifiabilityAnnotation() { + // When running under ModifiabilityChecker, GrowChecker handles custom type warnings. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index 84e690f04b9f..bef513d1944b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -26,4 +26,10 @@ protected boolean shouldCheckUnmodParamLocation() { // When running under ModifiabilityChecker, GrowChecker handles @UnmodParam location reporting. return checker.getParentChecker() == null; } + + @Override + protected boolean shouldCheckCustomModifiabilityAnnotation() { + // When running under ModifiabilityChecker, GrowChecker handles custom type warnings. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index d9669b0d2b64..99302158fae8 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1,2 +1,3 @@ usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. unmodparam.location=@UnmodParam may only be written on method/constructor parameters or explicit receiver parameters. +modifiability.annotation.unverified=Modifiability annotations/defaults on source-defined collection/map/iterator type %s are trusted but not verified by the checker; manually verify them and suppress this warning if correct. diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java new file mode 100644 index 000000000000..cd606d2f919c --- /dev/null +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -0,0 +1,54 @@ +import java.util.AbstractList; +import java.util.Collection; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Map; + +public class CustomModifiabilityAnnotationWarning { + + // :: warning: [modifiability.annotation.unverified] + static class ExtendsAbstractList extends AbstractList { + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + // :: warning: [modifiability.annotation.unverified] + abstract static class DirectCollection implements Collection {} + + // :: warning: [modifiability.annotation.unverified] + abstract static class DirectMap implements Map {} + + // :: warning: [modifiability.annotation.unverified] + abstract static class DirectIterator implements Iterator {} + + // :: warning: [modifiability.annotation.unverified] + abstract static class DirectListIterator implements ListIterator {} + + static class NonModifiabilityType {} + + @SuppressWarnings("modifiability:annotation.unverified") + static class SuppressedList extends AbstractList { + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + @SuppressWarnings("modifiability:annotation.unverified") + abstract static class SuppressedMap implements Map {} + + @SuppressWarnings("modifiability:annotation.unverified") + abstract static class SuppressedIterator implements Iterator {} +} diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 6d52734c7f8d..a334ae36e684 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -318,9 +318,11 @@ void copyOnWriteDirectOverridesRemainUsableButInheritedIteratorBasedApisDoNot() asList.replaceAll(s -> s); } + // :: warning: [modifiability.annotation.unverified] static class IteratorRemoveUnsupportedCollection extends AbstractCollection { @Override public Iterator iterator() { + // :: warning: [modifiability.annotation.unverified] return new Iterator<>() { private boolean hasNext = true; @@ -346,6 +348,7 @@ public int size() { } } + // :: warning: [modifiability.annotation.unverified] static class IteratorRemoveUnsupportedSet extends AbstractSet { @Override public Iterator iterator() { @@ -358,9 +361,11 @@ public int size() { } } + // :: warning: [modifiability.annotation.unverified] static class IteratorMutationUnsupportedList extends AbstractSequentialList { @Override public ListIterator listIterator(int index) { + // :: warning: [modifiability.annotation.unverified] return new ListIterator<>() { private int cursor = index; diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 344eaa5a1588..912ed110c3ea 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -103,6 +103,7 @@ public void arrayAccessReceiverSideEffect(List other) { // effects, so the behavior is correct regardless of @DoesNotUnrefineReceiver. // ========================================================== + // :: warning: [modifiability.annotation.unverified] static class ThisListReceiver extends ArrayList { public void update(@Modifiable ThisListReceiver this, List other) { diff --git a/framework/tests/all-systems/GenericsCasts.java b/framework/tests/all-systems/GenericsCasts.java index 2d9110ff4905..fd382c621b89 100644 --- a/framework/tests/all-systems/GenericsCasts.java +++ b/framework/tests/all-systems/GenericsCasts.java @@ -27,6 +27,7 @@ void m() { // are currently problematic. // TODO: try to find a problem with skipping this check. class Test { + @SuppressWarnings("modifiability:annotation.unverified") @Modifiable class Entry extends LinkedList {} class Queue { diff --git a/framework/tests/all-systems/GenericsEnclosing.java b/framework/tests/all-systems/GenericsEnclosing.java index 7aceb1881a19..3c5d6811e77e 100644 --- a/framework/tests/all-systems/GenericsEnclosing.java +++ b/framework/tests/all-systems/GenericsEnclosing.java @@ -7,6 +7,7 @@ * *

Also see regex/GenericsEnclosing for a test case for the Regex Checker. */ +@SuppressWarnings("modifiability:annotation.unverified") public @Modifiable class GenericsEnclosing extends TreeMap { class Inner { void foo() { diff --git a/framework/tests/all-systems/InferTypeArgs3.java b/framework/tests/all-systems/InferTypeArgs3.java index 3e5a51f26ee6..58ffdb32f349 100644 --- a/framework/tests/all-systems/InferTypeArgs3.java +++ b/framework/tests/all-systems/InferTypeArgs3.java @@ -12,6 +12,7 @@ void test2() { java.util.Arrays.asList(i, ""); } + @SuppressWarnings("modifiability:annotation.unverified") void foo() { new HashSet<>(Arrays.asList(new Object())); new HashSet(Arrays.asList(new Object())) {}; diff --git a/framework/tests/all-systems/Issue6810.java b/framework/tests/all-systems/Issue6810.java index 40a6b1c34389..09897ff797c0 100644 --- a/framework/tests/all-systems/Issue6810.java +++ b/framework/tests/all-systems/Issue6810.java @@ -4,6 +4,7 @@ public class Issue6810 { static class Box> {} + @SuppressWarnings("modifiability:annotation.unverified") abstract static class BoxSet> implements Set {} static > void intersect2( diff --git a/framework/tests/all-systems/RawTypeAssignment.java b/framework/tests/all-systems/RawTypeAssignment.java index b42d16dad10f..4f1675b89696 100644 --- a/framework/tests/all-systems/RawTypeAssignment.java +++ b/framework/tests/all-systems/RawTypeAssignment.java @@ -4,6 +4,7 @@ class Component {} +@SuppressWarnings("modifiability:annotation.unverified") @Modifiable class Components extends ArrayList {} // If we include a type parameter in the superclass, then there is no error below. diff --git a/framework/tests/all-systems/java8inference/Issue1715.java b/framework/tests/all-systems/java8inference/Issue1715.java index 948329fb757b..b7f8b120eb02 100644 --- a/framework/tests/all-systems/java8inference/Issue1715.java +++ b/framework/tests/all-systems/java8inference/Issue1715.java @@ -23,6 +23,7 @@ static Observable just(T param) { } } + @SuppressWarnings("modifiability:annotation.unverified") public abstract static class ImmutableList implements List { public static ImmutableList of(E element) { diff --git a/framework/tests/all-systems/java8inference/Issue3036.java b/framework/tests/all-systems/java8inference/Issue3036.java index 2d074ff3ac3f..923fe624644e 100644 --- a/framework/tests/all-systems/java8inference/Issue3036.java +++ b/framework/tests/all-systems/java8inference/Issue3036.java @@ -43,6 +43,7 @@ private void write(Stream stream) { .collect(Collectors.toList()); } + @SuppressWarnings("modifiability:annotation.unverified") public static @Modifiable class ImmutableMap extends HashMap { public static ImmutableMap of(K k1, V v1, K k2, V v2) { throw new RuntimeException(); diff --git a/framework/tests/all-systems/java8inference/MemRefInfere.java b/framework/tests/all-systems/java8inference/MemRefInfere.java index 0ddd3bd9060e..53ecb78e6a97 100644 --- a/framework/tests/all-systems/java8inference/MemRefInfere.java +++ b/framework/tests/all-systems/java8inference/MemRefInfere.java @@ -8,6 +8,7 @@ import java.util.stream.Collector; import java.util.stream.Collectors; +@SuppressWarnings("modifiability:annotation.unverified") public abstract class MemRefInfere implements Map, Serializable { public static MemRefInfere copyOf(Map map) { throw new RuntimeException(); From 12d6bfcd4ca02cc199a57acf163104a5257af379 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 23 May 2026 13:33:34 -0700 Subject: [PATCH 176/308] Add test for `Map.getOrDefault()` --- .../tests/modifiability/GetOrDefaultTest.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 checker/tests/modifiability/GetOrDefaultTest.java diff --git a/checker/tests/modifiability/GetOrDefaultTest.java b/checker/tests/modifiability/GetOrDefaultTest.java new file mode 100644 index 000000000000..32dccc5efea6 --- /dev/null +++ b/checker/tests/modifiability/GetOrDefaultTest.java @@ -0,0 +1,130 @@ +import java.util.Date; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +interface MyGodMap { + // The actual definition. + V getOrDefault(MyGodMap this, Object key, V defaultValue); + + // With @PolyModifiable, which seems wrong. + @PolyModifiable V getOrDefaultPoly(MyGodMap this, Object key, @PolyModifiable V defaultValue); + + // Using a second type variable. + V2 getOrDefault2(MyGodMap this, Object key, V2 defaultValue); +} + +public class GetOrDefaultTest { + + Date d; + @Modifiable Date dm; + @Unmodifiable Date du; + + void foo1(MyGodMap m) { + Date d1 = m.getOrDefault("hello", d); + @Modifiable Date d2 = m.getOrDefault("hello", d); + @Unmodifiable Date d3 = m.getOrDefault("hello", d); + Date d4 = m.getOrDefault("hello", dm); + @Modifiable Date d5 = m.getOrDefault("hello", dm); + @Unmodifiable Date d6 = m.getOrDefault("hello", dm); + Date d7 = m.getOrDefault("hello", du); + @Modifiable Date d8 = m.getOrDefault("hello", du); + @Unmodifiable Date d9 = m.getOrDefault("hello", du); + } + + void foo2(MyGodMap m) { + Date d1 = m.getOrDefault("hello", d); + @Modifiable Date d2 = m.getOrDefault("hello", d); + @Unmodifiable Date d3 = m.getOrDefault("hello", d); + Date d4 = m.getOrDefault("hello", dm); + @Modifiable Date d5 = m.getOrDefault("hello", dm); + @Unmodifiable Date d6 = m.getOrDefault("hello", dm); + Date d7 = m.getOrDefault("hello", du); + @Modifiable Date d8 = m.getOrDefault("hello", du); + @Unmodifiable Date d9 = m.getOrDefault("hello", du); + } + + void foo3(MyGodMap m) { + Date d1 = m.getOrDefault("hello", d); + @Modifiable Date d2 = m.getOrDefault("hello", d); + @Unmodifiable Date d3 = m.getOrDefault("hello", d); + Date d4 = m.getOrDefault("hello", dm); + @Modifiable Date d5 = m.getOrDefault("hello", dm); + @Unmodifiable Date d6 = m.getOrDefault("hello", dm); + Date d7 = m.getOrDefault("hello", du); + @Modifiable Date d8 = m.getOrDefault("hello", du); + @Unmodifiable Date d9 = m.getOrDefault("hello", du); + } + + void foo4(MyGodMap m) { + Date d1 = m.getOrDefaultPoly("hello", d); + @Modifiable Date d2 = m.getOrDefaultPoly("hello", d); + @Unmodifiable Date d3 = m.getOrDefaultPoly("hello", d); + Date d4 = m.getOrDefaultPoly("hello", dm); + @Modifiable Date d5 = m.getOrDefaultPoly("hello", dm); + @Unmodifiable Date d6 = m.getOrDefaultPoly("hello", dm); + Date d7 = m.getOrDefaultPoly("hello", du); + @Modifiable Date d8 = m.getOrDefaultPoly("hello", du); + @Unmodifiable Date d9 = m.getOrDefaultPoly("hello", du); + } + + void foo5(MyGodMap m) { + Date d1 = m.getOrDefaultPoly("hello", d); + @Modifiable Date d2 = m.getOrDefaultPoly("hello", d); + @Unmodifiable Date d3 = m.getOrDefaultPoly("hello", d); + Date d4 = m.getOrDefaultPoly("hello", dm); + @Modifiable Date d5 = m.getOrDefaultPoly("hello", dm); + @Unmodifiable Date d6 = m.getOrDefaultPoly("hello", dm); + Date d7 = m.getOrDefaultPoly("hello", du); + @Modifiable Date d8 = m.getOrDefaultPoly("hello", du); + @Unmodifiable Date d9 = m.getOrDefaultPoly("hello", du); + } + + void foo6(MyGodMap m) { + Date d1 = m.getOrDefaultPoly("hello", d); + @Modifiable Date d2 = m.getOrDefaultPoly("hello", d); + @Unmodifiable Date d3 = m.getOrDefaultPoly("hello", d); + Date d4 = m.getOrDefaultPoly("hello", dm); + @Modifiable Date d5 = m.getOrDefaultPoly("hello", dm); + @Unmodifiable Date d6 = m.getOrDefaultPoly("hello", dm); + Date d7 = m.getOrDefaultPoly("hello", du); + @Modifiable Date d8 = m.getOrDefaultPoly("hello", du); + @Unmodifiable Date d9 = m.getOrDefaultPoly("hello", du); + } + + void foo7(MyGodMap m) { + Date d1 = m.getOrDefault2("hello", d); + @Modifiable Date d2 = m.getOrDefault2("hello", d); + @Unmodifiable Date d3 = m.getOrDefault2("hello", d); + Date d4 = m.getOrDefault2("hello", dm); + @Modifiable Date d5 = m.getOrDefault2("hello", dm); + @Unmodifiable Date d6 = m.getOrDefault2("hello", dm); + Date d7 = m.getOrDefault2("hello", du); + @Modifiable Date d8 = m.getOrDefault2("hello", du); + @Unmodifiable Date d9 = m.getOrDefault2("hello", du); + } + + void foo8(MyGodMap m) { + Date d1 = m.getOrDefault2("hello", d); + @Modifiable Date d2 = m.getOrDefault2("hello", d); + @Unmodifiable Date d3 = m.getOrDefault2("hello", d); + Date d4 = m.getOrDefault2("hello", dm); + @Modifiable Date d5 = m.getOrDefault2("hello", dm); + @Unmodifiable Date d6 = m.getOrDefault2("hello", dm); + Date d7 = m.getOrDefault2("hello", du); + @Modifiable Date d8 = m.getOrDefault2("hello", du); + @Unmodifiable Date d9 = m.getOrDefault2("hello", du); + } + + void foo9(MyGodMap m) { + Date d1 = m.getOrDefault2("hello", d); + @Modifiable Date d2 = m.getOrDefault2("hello", d); + @Unmodifiable Date d3 = m.getOrDefault2("hello", d); + Date d4 = m.getOrDefault2("hello", dm); + @Modifiable Date d5 = m.getOrDefault2("hello", dm); + @Unmodifiable Date d6 = m.getOrDefault2("hello", dm); + Date d7 = m.getOrDefault2("hello", du); + @Modifiable Date d8 = m.getOrDefault2("hello", du); + @Unmodifiable Date d9 = m.getOrDefault2("hello", du); + } +} From e3aff7c980f783c88ca254ab7d722c619cd7217e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 23 May 2026 21:21:16 -0700 Subject: [PATCH 177/308] Rename modifiability qualifiers --- .../afu/scenelib/util/coll/WrapperMap.java | 6 +- buildSrc/src/main/groovy/cf-spotless.gradle | 18 ++--- .../{BottomGrow.java => BottomGrowable.java} | 2 +- ...tomReplace.java => BottomReplaceable.java} | 2 +- ...ottomShrink.java => BottomShrinkable.java} | 2 +- .../checker/modifiability/qual/Growable.java | 2 +- .../{MaybeGrow.java => MaybeGrowable.java} | 2 +- .../modifiability/qual/MaybeModifiable.java | 8 +- ...aybeReplace.java => MaybeReplaceable.java} | 2 +- ...{MaybeShrink.java => MaybeShrinkable.java} | 2 +- .../modifiability/qual/Modifiable.java | 2 +- .../qual/{PolyGrow.java => PolyGrowable.java} | 6 +- .../modifiability/qual/PolyModifiable.java | 4 +- ...{PolyReplace.java => PolyReplaceable.java} | 6 +- .../{PolyShrink.java => PolyShrinkable.java} | 4 +- .../modifiability/qual/Replaceable.java | 2 +- .../modifiability/qual/Shrinkable.java | 2 +- .../modifiability/qual/Ungrowable.java | 2 +- .../modifiability/qual/UnmodParam.java | 10 +-- .../modifiability/qual/Unmodifiable.java | 2 +- .../modifiability/qual/Unreplaceable.java | 2 +- .../modifiability/qual/Unshrinkable.java | 2 +- .../grow/GrowAnnotatedTypeFactory.java | 38 +++++----- .../modifiability/grow/GrowChecker.java | 4 +- .../replace/ReplaceAnnotatedTypeFactory.java | 32 ++++---- .../modifiability/replace/ReplaceChecker.java | 2 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 41 +++++----- .../modifiability/shrink/ShrinkChecker.java | 4 +- .../AliasCanonicalizationTest.java | 18 ++--- .../tests/modifiability/HierarchyTest.java | 75 +++++++++--------- .../modifiability/IteratorPrecisionTest.java | 2 +- .../modifiability/PolyModifiableTest.java | 16 ++-- .../tests/modifiability/PolyShrinkTest.java | 4 +- checker/tests/modifiability/SetTest.java | 68 +++++++++-------- docs/manual/modifiability-checker.tex | 76 +++++++++---------- 35 files changed, 238 insertions(+), 232 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{BottomGrow.java => BottomGrowable.java} (94%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{BottomReplace.java => BottomReplaceable.java} (94%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{BottomShrink.java => BottomShrinkable.java} (94%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{MaybeGrow.java => MaybeGrowable.java} (96%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{MaybeReplace.java => MaybeReplaceable.java} (96%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{MaybeShrink.java => MaybeShrinkable.java} (96%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{PolyGrow.java => PolyGrowable.java} (83%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{PolyReplace.java => PolyReplaceable.java} (88%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{PolyShrink.java => PolyShrinkable.java} (90%) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index ef950e8e6b21..dce3e04ee2c1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -4,7 +4,7 @@ import java.util.Map; import java.util.Set; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.PolyShrink; +import org.checkerframework.checker.modifiability.qual.PolyShrinkable; /** * A {@link WrapperMap} is a map all of whose methods delegate by default to those of a supplied @@ -40,8 +40,8 @@ public boolean containsValue(Object value) { @SuppressWarnings("keyfor") // use of delegate object @Override - public @IteratorPolyMod @PolyShrink Set> entrySet( - @PolyShrink WrapperMap this) { + public @IteratorPolyMod @PolyShrinkable Set> entrySet( + @PolyShrinkable WrapperMap this) { return back.entrySet(); } diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 5c375969a4a8..b3c6a526401d 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -92,24 +92,24 @@ spotless { googleJavaFormat(libs.google.java.format.get().version) } formatAnnotations() - .addTypeAnnotation("BottomGrow") - .addTypeAnnotation("BottomReplace") - .addTypeAnnotation("BottomShrink") + .addTypeAnnotation("BottomGrowable") + .addTypeAnnotation("BottomReplaceable") + .addTypeAnnotation("BottomShrinkable") .addTypeAnnotation("Growable") .addTypeAnnotation("IteratorPolyMod") .addTypeAnnotation("Modifiable") - .addTypeAnnotation("PolyGrow") + .addTypeAnnotation("PolyGrowable") .addTypeAnnotation("PolyModifiable") - .addTypeAnnotation("PolyReplace") - .addTypeAnnotation("PolyShrink") + .addTypeAnnotation("PolyReplaceable") + .addTypeAnnotation("PolyShrinkable") .addTypeAnnotation("Replaceable") .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") - .addTypeAnnotation("MaybeGrow") + .addTypeAnnotation("MaybeGrowable") .addTypeAnnotation("MaybeIteratorPolyMod") .addTypeAnnotation("MaybeModifiable") - .addTypeAnnotation("MaybeReplace") - .addTypeAnnotation("MaybeShrink") + .addTypeAnnotation("MaybeReplaceable") + .addTypeAnnotation("MaybeShrinkable") .addTypeAnnotation("Unmodifiable") .addTypeAnnotation("UnmodParam") .addTypeAnnotation("Unreplaceable") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrowable.java similarity index 94% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrowable.java index a01b989a8664..74583859f068 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrow.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrowable.java @@ -18,4 +18,4 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({Growable.class, Ungrowable.class}) -public @interface BottomGrow {} +public @interface BottomGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplaceable.java similarity index 94% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplaceable.java index a2609e3558de..13bcf09aa392 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplaceable.java @@ -18,4 +18,4 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({Replaceable.class, Unreplaceable.class}) -public @interface BottomReplace {} +public @interface BottomReplaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrinkable.java similarity index 94% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrinkable.java index 3cf5d6902f6e..b27c14d30ad7 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrinkable.java @@ -18,4 +18,4 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({Shrinkable.class, Unshrinkable.class}) -public @interface BottomShrink {} +public @interface BottomShrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java index 2c8041469c75..4167df7d64b9 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeGrow.class) +@SubtypeOf(MaybeGrowable.class) public @interface Growable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrow.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java index 92789632cc75..628c95dc13c7 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrow.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface MaybeGrow {} +public @interface MaybeGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java index 5a9a53e8f45c..c4b7e274efda 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java @@ -7,12 +7,12 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @MaybeGrow @MaybeShrink @MaybeReplace}. Represents an unknown or - * arbitrary modifiability capability; the checker cannot determine whether the collection is - * growable, shrinkable, or replaceable. + * Convenience alias meaning {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable}. Represents + * an unknown or arbitrary modifiability capability; the checker cannot determine whether the + * collection is growable, shrinkable, or replaceable. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @MaybeGrow @MaybeShrink @MaybeReplace} on each annotated type. + * {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplace.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java index 70fbbb939007..641d51b83b56 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java @@ -22,4 +22,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface MaybeReplace {} +public @interface MaybeReplaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java index d5edc2ccf523..b6889579408b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java @@ -22,4 +22,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface MaybeShrink {} +public @interface MaybeShrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 2d5f571deb64..f02de1c0592c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -13,7 +13,7 @@ * *

As an exception, {@code @Modifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Modifiable Iterator} means - * {@code @MaybeGrow @Shrinkable @MaybeReplace Iterator}. + * {@code @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrowable.java similarity index 83% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrowable.java index ef00276e8d64..fe6467671535 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrow.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrowable.java @@ -11,7 +11,7 @@ * A polymorphic qualifier for the Grow hierarchy. * *

When used on a method, the Grow capability of the return type matches the Grow capability of - * the argument or receiver annotated with {@code @PolyGrow}. + * the argument or receiver annotated with {@code @PolyGrowable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(MaybeGrow.class) -public @interface PolyGrow {} +@PolymorphicQualifier(MaybeGrowable.class) +public @interface PolyGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 87345ae94317..517762db7857 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -4,8 +4,8 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @PolyGrow @PolyReplace @PolyShrink}. A polymorphic qualifier for - * all three modifiability hierarchies. + * Convenience alias meaning {@code @PolyGrowable @PolyReplaceable @PolyShrinkable}. A polymorphic + * qualifier for all three modifiability hierarchies. * *

You should write {@code @PolyModifiable} on methods that preserve or transfer modifiability, * such as {@code List.subList()}, {@code iterator()}, and {@code stream()}. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplaceable.java similarity index 88% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplaceable.java index f8664f330bca..b9f7b0fa5815 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplaceable.java @@ -11,7 +11,7 @@ * A polymorphic qualifier for the Replace hierarchy. * *

When used on a method, the Replace capability of the return type matches the Replace - * capability of the argument or receiver annotated with {@code @PolyReplace}. + * capability of the argument or receiver annotated with {@code @PolyReplaceable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(MaybeReplace.class) -public @interface PolyReplace {} +@PolymorphicQualifier(MaybeReplaceable.class) +public @interface PolyReplaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrinkable.java similarity index 90% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrinkable.java index f35eece7403b..9ed1fef83e6b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrink.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrinkable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(MaybeShrink.class) -public @interface PolyShrink {} +@PolymorphicQualifier(MaybeShrinkable.class) +public @interface PolyShrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java index 2d7c2e7c6926..9759df8a08b4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeReplace.class) +@SubtypeOf(MaybeReplaceable.class) public @interface Replaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java index 1d7fddbeea03..10af526c558c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeShrink.class) +@SubtypeOf(MaybeShrinkable.class) public @interface Shrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java index d160c345b0e4..fba98d025e2f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Ungrowable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeGrow.class) +@SubtypeOf(MaybeGrowable.class) public @interface Ungrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java index e065abdb7a5f..7bde3f044d28 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java @@ -7,13 +7,13 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @MaybeGrow @MaybeShrink @MaybeReplace}. This is a syntactic - * sugar for {@link MaybeModifiable}. It is intended to be used on parameters of methods that are - * not designed to change the parameter. This annotation is only allowed to be written within method - * or constructor parameter types, or explicit receiver parameter types. + * Convenience alias meaning {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable}. This is a + * syntactic sugar for {@link MaybeModifiable}. It is intended to be used on parameters of methods + * that are not designed to change the parameter. This annotation is only allowed to be written + * within method or constructor parameter types, or explicit receiver parameter types. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @MaybeGrow @MaybeShrink @MaybeReplace} on each annotated type. + * {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable} on each annotated type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 53ea1e4066ff..2e233b798c11 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -13,7 +13,7 @@ * *

As an exception, {@code @Unmodifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Unmodifiable Iterator} means - * {@code @MaybeGrow @Unshrinkable @MaybeReplace Iterator}. + * {@code @MaybeGrowable @Unshrinkable @MaybeReplaceable Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java index 32dc1f2e6a47..eec0205d2c50 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unreplaceable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeReplace.class) +@SubtypeOf(MaybeReplaceable.class) public @interface Unreplaceable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java index 347db2a2b656..c52ce9679d8a 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java @@ -18,5 +18,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeShrink.class) +@SubtypeOf(MaybeShrinkable.class) public @interface Unshrinkable {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 55790b56e58f..679e46918be3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -12,14 +12,14 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; -import org.checkerframework.checker.modifiability.qual.BottomGrow; +import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.MaybeGrow; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyGrow; +import org.checkerframework.checker.modifiability.qual.PolyGrowable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.UnmodParam; @@ -48,7 +48,7 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link MaybeGrow} qualifier (top of Grow hierarchy). */ + /** The {@code @}{@link MaybeGrowable} qualifier (top of Grow hierarchy). */ private final AnnotationMirror MAYBE_GROW; /** The {@code @}{@link Growable} qualifier. */ @@ -57,7 +57,7 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link Ungrowable} qualifier. */ private final AnnotationMirror UNGROWABLE; - /** The {@code @}{@link PolyGrow} qualifier. */ + /** The {@code @}{@link PolyGrowable} qualifier. */ private final AnnotationMirror POLY_GROW; /** The {@code @}{@link IteratorPolyMod} qualifier. */ @@ -80,10 +80,10 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.listIteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_GROW = AnnotationBuilder.fromClass(getElementUtils(), MaybeGrow.class); + this.MAYBE_GROW = AnnotationBuilder.fromClass(getElementUtils(), MaybeGrowable.class); this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); - this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrow.class); + this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrowable.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); @@ -94,11 +94,11 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - MaybeGrow.class, + MaybeGrowable.class, Growable.class, Ungrowable.class, - BottomGrow.class, - PolyGrow.class, + BottomGrowable.class, + PolyGrowable.class, MaybeIteratorPolyMod.class, IteratorPolyMod.class)); } @@ -109,13 +109,13 @@ protected Set> createSupportedTypeQualifiers() { * *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that cannot grow structurally, such as {@code Map.Entry} and non-{@code ListIterator} {@code - * Iterator}, their grow component canonicalizes to {@code @MaybeGrow}. Explicit grow qualifiers - * are left to normal canonicalization so users can still write and preserve an explicit - * capability tuple such as {@code @Growable @Shrinkable @Replaceable Iterator}. + * Iterator}, their grow component canonicalizes to {@code @MaybeGrowable}. Explicit grow + * qualifiers are left to normal canonicalization so users can still write and preserve an + * explicit capability tuple such as {@code @Growable @Shrinkable @Replaceable Iterator}. * - *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrow}, but for - * {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its grow - * bit is therefore {@code @MaybeGrow}. + *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrowable}, but + * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its + * grow bit is therefore {@code @MaybeGrowable}. */ @Override public AnnotationMirror canonicalAnnotation( @@ -158,14 +158,14 @@ protected ParameterizedExecutableType methodFromUse( * ArrayList. A stub annotation like * *

{@code
-   * static  @PolyGrow List withoutDuplicates(@PolyGrow Collection values)
+   * static  @PolyGrowable List withoutDuplicates(@PolyGrowable Collection values)
    * }
* * would be unsound, because an {@code @Ungrowable} input could receive the fresh ArrayList, whose * static type should be {@code @Growable}. It would also be too imprecise to always use - * {@code @MaybeGrow}, because passing a {@code @Growable} collection guarantees that both + * {@code @MaybeGrowable}, because passing a {@code @Growable} collection guarantees that both * possible results are growable. Therefore, model the method here as preserving {@code @Growable} - * inputs and otherwise returning {@code @MaybeGrow}. + * inputs and otherwise returning {@code @MaybeGrowable}. * * @param tree the invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index cefbe5f6d2e0..c4095808193b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a grow method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Growable} collections can be - * safely added to, {@code @Ungrowable} collections cannot, and {@code @MaybeGrow} collections have - * unknown grow behavior. + * safely added to, {@code @Ungrowable} collections cannot, and {@code @MaybeGrowable} collections + * have unknown grow behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 04a4672e2a4f..cd6b2d0c0f82 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -12,14 +12,14 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; -import org.checkerframework.checker.modifiability.qual.BottomReplace; +import org.checkerframework.checker.modifiability.qual.BottomReplaceable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; -import org.checkerframework.checker.modifiability.qual.MaybeReplace; +import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; -import org.checkerframework.checker.modifiability.qual.PolyReplace; +import org.checkerframework.checker.modifiability.qual.PolyReplaceable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -57,7 +57,7 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link MaybeReplace} qualifier (top of Replace hierarchy). */ + /** The {@code @}{@link MaybeReplaceable} qualifier (top of Replace hierarchy). */ private final AnnotationMirror MAYBE_REPLACE; /** The {@code @}{@link Replaceable} qualifier. */ @@ -66,7 +66,7 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link Unreplaceable} qualifier. */ private final AnnotationMirror UNREPLACEABLE; - /** The {@code @}{@link PolyReplace} qualifier. */ + /** The {@code @}{@link PolyReplaceable} qualifier. */ private final AnnotationMirror POLY_REPLACE; /** The {@code @}{@link IteratorPolyMod} qualifier. */ @@ -94,10 +94,10 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), MaybeReplace.class); + this.MAYBE_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), MaybeReplaceable.class); this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); - this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplace.class); + this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplaceable.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); @@ -108,11 +108,11 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - MaybeReplace.class, + MaybeReplaceable.class, Replaceable.class, Unreplaceable.class, - BottomReplace.class, - PolyReplace.class, + BottomReplaceable.class, + PolyReplaceable.class, MaybeIteratorPolyMod.class, IteratorPolyMod.class)); } @@ -124,11 +124,11 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that cannot replace structurally, such as exact {@code Collection}, {@code Set}, non-{@code * LinkedList} {@code Queue}, and non-{@code ListIterator} {@code Iterator}, their replace - * component canonicalizes to {@code @MaybeReplace}. Explicit replace qualifiers are left to + * component canonicalizes to {@code @MaybeReplaceable}. Explicit replace qualifiers are left to * normal canonicalization so users can still write and preserve an explicit capability tuple such * as {@code @Growable @Shrinkable @Replaceable Set}. * - *

{@code @PolyModifiable} remains {@code @PolyReplace}: unlike grow and shrink for {@code + *

{@code @PolyModifiable} remains {@code @PolyReplaceable}: unlike grow and shrink for {@code * Map.Entry}, replacement through {@code Entry.setValue} is a meaningful capability to carry from * the map receiver. */ @@ -173,14 +173,14 @@ protected ParameterizedExecutableType methodFromUse( * ArrayList. A stub annotation like * *

{@code
-   * static  @PolyReplace List withoutDuplicates(@PolyReplace Collection values)
+   * static  @PolyReplaceable List withoutDuplicates(@PolyReplaceable Collection values)
    * }
* * would be unsound, because an {@code @Unreplaceable} input could receive the fresh ArrayList, * whose static type should be {@code @Replaceable}. It would also be too imprecise to always use - * {@code @MaybeReplace}, because passing a {@code @Replaceable} collection guarantees that both - * possible results are replaceable. Therefore, model the method here as preserving - * {@code @Replaceable} inputs and otherwise returning {@code @MaybeReplace}. + * {@code @MaybeReplaceable}, because passing a {@code @Replaceable} collection guarantees that + * both possible results are replaceable. Therefore, model the method here as preserving + * {@code @Replaceable} inputs and otherwise returning {@code @MaybeReplaceable}. * * @param tree the invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index f5a38440590c..273af4537da6 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -15,7 +15,7 @@ * *

The checker enforces the Modifiability type system, where {@code @Replaceable} collections can * be safely mutated, {@code @Unreplaceable} collections cannot be replaced, and - * {@code @MaybeReplace} collections have unknown replace behavior. + * {@code @MaybeReplaceable} collections have unknown replace behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 721dd3d885fd..8e6092cffc7c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -12,14 +12,14 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; -import org.checkerframework.checker.modifiability.qual.BottomShrink; +import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; -import org.checkerframework.checker.modifiability.qual.MaybeShrink; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; -import org.checkerframework.checker.modifiability.qual.PolyShrink; +import org.checkerframework.checker.modifiability.qual.PolyShrinkable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -46,7 +46,7 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link MaybeShrink} qualifier (top of Shrink hierarchy). */ + /** The {@code @}{@link MaybeShrinkable} qualifier (top of Shrink hierarchy). */ private final AnnotationMirror MAYBE_SHRINK; /** The {@code @}{@link Shrinkable} qualifier. */ @@ -55,7 +55,7 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link Unshrinkable} qualifier. */ private final AnnotationMirror UNSHRINKABLE; - /** The {@code @}{@link PolyShrink} qualifier. */ + /** The {@code @}{@link PolyShrinkable} qualifier. */ private final AnnotationMirror POLY_SHRINK; /** The {@code @}{@link IteratorPolyMod} qualifier. */ @@ -77,10 +77,10 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrink.class); + this.MAYBE_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrinkable.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); - this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrink.class); + this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrinkable.class); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); @@ -91,11 +91,11 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( - MaybeShrink.class, + MaybeShrinkable.class, Shrinkable.class, Unshrinkable.class, - BottomShrink.class, - PolyShrink.class, + BottomShrinkable.class, + PolyShrinkable.class, MaybeIteratorPolyMod.class, IteratorPolyMod.class)); } @@ -105,13 +105,14 @@ protected Set> createSupportedTypeQualifiers() { * aliases whose meaning depends on the annotated type. * *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on {@code - * Map.Entry}, which cannot shrink, their shrink component canonicalizes to {@code @MaybeShrink}. - * Explicit shrink qualifiers are left to normal canonicalization so users can still write and - * preserve an explicit capability tuple such as {@code @Growable @Shrinkable @Replaceable Entry}. + * Map.Entry}, which cannot shrink, their shrink component canonicalizes to + * {@code @MaybeShrinkable}. Explicit shrink qualifiers are left to normal canonicalization so + * users can still write and preserve an explicit capability tuple such as + * {@code @Growable @Shrinkable @Replaceable Entry}. * - *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrink}, but for - * {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its shrink - * bit is therefore {@code @MaybeShrink}. + *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrinkable}, but + * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its + * shrink bit is therefore {@code @MaybeShrinkable}. */ @Override public AnnotationMirror canonicalAnnotation( @@ -154,14 +155,14 @@ protected ParameterizedExecutableType methodFromUse( * ArrayList. A stub annotation like * *

{@code
-   * static  @PolyShrink List withoutDuplicates(@PolyShrink Collection values)
+   * static  @PolyShrinkable List withoutDuplicates(@PolyShrinkable Collection values)
    * }
* * would be unsound, because an {@code @Unshrinkable} input could receive the fresh ArrayList, * whose static type should be {@code @Shrinkable}. It would also be too imprecise to always use - * {@code @MaybeShrink}, because passing a {@code @Shrinkable} collection guarantees that both + * {@code @MaybeShrinkable}, because passing a {@code @Shrinkable} collection guarantees that both * possible results are shrinkable. Therefore, model the method here as preserving - * {@code @Shrinkable} inputs and otherwise returning {@code @MaybeShrink}. + * {@code @Shrinkable} inputs and otherwise returning {@code @MaybeShrinkable}. * * @param tree the invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method @@ -182,7 +183,7 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( * *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked * method receiver type has {@code @IteratorPolyMod}, then the result is {@code @Shrinkable - * Iterator}. Otherwise, shrinkability precision is dropped to {@code @MaybeShrink}. + * Iterator}. Otherwise, shrinkability precision is dropped to {@code @MaybeShrinkable}. * * @param tree the iterator method invocation * @param methodType the annotated executable type of the invoked method diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index 0e5894ee4ea3..cafa8a471d87 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -14,8 +14,8 @@ * UnsupportedOperationException} at run time due to calling a shrink method on a collection. * *

The checker enforces the Modifiability type system, where {@code @Shrinkable} collections can - * be safely mutated, {@code @Unshrinkable} collections cannot be shrunk, and {@code @MaybeShrink} - * collections have unknown shrink behavior. + * be safely mutated, {@code @Unshrinkable} collections cannot be shrunk, and + * {@code @MaybeShrinkable} collections have unknown shrink behavior. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/tests/modifiability/AliasCanonicalizationTest.java b/checker/tests/modifiability/AliasCanonicalizationTest.java index 88ae53220063..a5d410f36be5 100644 --- a/checker/tests/modifiability/AliasCanonicalizationTest.java +++ b/checker/tests/modifiability/AliasCanonicalizationTest.java @@ -5,9 +5,9 @@ import java.util.Queue; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.MaybeGrow; -import org.checkerframework.checker.modifiability.qual.MaybeReplace; -import org.checkerframework.checker.modifiability.qual.MaybeShrink; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; +import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; @@ -17,37 +17,37 @@ class AliasCanonicalizationTest { void modifiableIterator(@Modifiable Iterator iterator) { - @MaybeGrow @Shrinkable @MaybeReplace Iterator canonical = iterator; + @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator canonical = iterator; @Shrinkable Iterator shrinkable = iterator; iterator.remove(); } void unmodifiableIterator(@Unmodifiable Iterator iterator) { - @MaybeGrow @Unshrinkable @MaybeReplace Iterator canonical = iterator; + @MaybeGrowable @Unshrinkable @MaybeReplaceable Iterator canonical = iterator; // :: error: [method.invocation] iterator.remove(); } void modifiableSet(@Modifiable Set set) { - @Growable @Shrinkable @MaybeReplace Set canonical = set; + @Growable @Shrinkable @MaybeReplaceable Set canonical = set; set.add("a"); set.remove("a"); } void modifiableQueue(@Modifiable Queue queue) { - @Growable @Shrinkable @MaybeReplace Queue canonical = queue; + @Growable @Shrinkable @MaybeReplaceable Queue canonical = queue; queue.add("a"); queue.remove(); } void modifiableCollection(@Modifiable Collection collection) { - @Growable @Shrinkable @MaybeReplace Collection canonical = collection; + @Growable @Shrinkable @MaybeReplaceable Collection canonical = collection; collection.add("a"); collection.remove("a"); } void modifiableEntry(Map.@Modifiable Entry entry) { - Map.@MaybeGrow @MaybeShrink @Replaceable Entry canonical = entry; + Map.@MaybeGrowable @MaybeShrinkable @Replaceable Entry canonical = entry; entry.setValue("value"); } diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index 2eaf9158d8dd..b005b551464e 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -1,11 +1,11 @@ -import org.checkerframework.checker.modifiability.qual.BottomGrow; -import org.checkerframework.checker.modifiability.qual.BottomReplace; -import org.checkerframework.checker.modifiability.qual.BottomShrink; +import org.checkerframework.checker.modifiability.qual.BottomGrowable; +import org.checkerframework.checker.modifiability.qual.BottomReplaceable; +import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.MaybeGrow; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; -import org.checkerframework.checker.modifiability.qual.MaybeReplace; -import org.checkerframework.checker.modifiability.qual.MaybeShrink; +import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; @@ -26,10 +26,13 @@ class HierarchyTest { void testGrow( - @Growable Object g, @Ungrowable Object ug, @MaybeGrow Object u, @BottomGrow Object b) { - @MaybeGrow Object u1 = g; - @MaybeGrow Object u2 = ug; - @MaybeGrow Object u3 = b; + @Growable Object g, + @Ungrowable Object ug, + @MaybeGrowable Object u, + @BottomGrowable Object b) { + @MaybeGrowable Object u1 = g; + @MaybeGrowable Object u2 = ug; + @MaybeGrowable Object u3 = b; @Growable Object g1 = b; @Ungrowable Object ug1 = b; @@ -43,21 +46,21 @@ void testGrow( // :: error: [assignment] @Ungrowable Object ug3 = u; // :: error: [assignment] - @BottomGrow Object b1 = g; + @BottomGrowable Object b1 = g; // :: error: [assignment] - @BottomGrow Object b2 = ug; + @BottomGrowable Object b2 = ug; // :: error: [assignment] - @BottomGrow Object b3 = u; + @BottomGrowable Object b3 = u; } void testShrink( @Shrinkable Object s, @Unshrinkable Object us, - @MaybeShrink Object u, - @BottomShrink Object b) { - @MaybeShrink Object u1 = s; - @MaybeShrink Object u2 = us; - @MaybeShrink Object u3 = b; + @MaybeShrinkable Object u, + @BottomShrinkable Object b) { + @MaybeShrinkable Object u1 = s; + @MaybeShrinkable Object u2 = us; + @MaybeShrinkable Object u3 = b; @Shrinkable Object s1 = b; @Unshrinkable Object us1 = b; @@ -75,11 +78,11 @@ void testShrink( void testReplace( @Replaceable Object r, @Unreplaceable Object ur, - @MaybeReplace Object u, - @BottomReplace Object b) { - @MaybeReplace Object u1 = r; - @MaybeReplace Object u2 = ur; - @MaybeReplace Object u3 = b; + @MaybeReplaceable Object u, + @BottomReplaceable Object b) { + @MaybeReplaceable Object u1 = r; + @MaybeReplaceable Object u2 = ur; + @MaybeReplaceable Object u3 = b; @Replaceable Object r1 = b; @Unreplaceable Object ur1 = b; @@ -103,7 +106,7 @@ void testCombinations( @Shrinkable Object s, @Replaceable Object r, @Ungrowable @Unshrinkable @Unreplaceable Object none, - @MaybeGrow @MaybeShrink @MaybeReplace Object unknown) { + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object unknown) { @Growable Object gv1 = gsr; @Growable Object gv2 = gs; @@ -147,15 +150,15 @@ void testCombinations( // :: error: [assignment] @Replaceable Object rv8 = unknown; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv1 = gsr; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv2 = gs; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv3 = gr; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv4 = sr; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv5 = g; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv6 = s; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv7 = r; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv8 = none; - @MaybeGrow @MaybeShrink @MaybeReplace Object tv9 = unknown; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv1 = gsr; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv2 = gs; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv3 = gr; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv4 = sr; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv5 = g; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv6 = s; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv7 = r; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv8 = none; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object tv9 = unknown; } void testAliases( @@ -169,9 +172,9 @@ void testAliases( @Unshrinkable Object us1 = unmod; @Unreplaceable Object ur1 = unmod; - @MaybeGrow Object ug2 = unknown; - @MaybeShrink Object us2 = unknown; - @MaybeReplace Object ur2 = unknown; + @MaybeGrowable Object ug2 = unknown; + @MaybeShrinkable Object us2 = unknown; + @MaybeReplaceable Object ur2 = unknown; @MaybeModifiable Object unknown1 = mod; @MaybeModifiable Object unknown2 = unmod; diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index a334ae36e684..eab62642ce4e 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -69,7 +69,7 @@ void copyOnWriteArrayListIterator() { CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); @Unshrinkable Iterator iterator = list.iterator(); - // store CopyOnWriteArrayList as a list, the iterator is @MaybeShrink. + // store CopyOnWriteArrayList as a list, the iterator is @MaybeShrinkable. List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index d11b9dd246b6..939ec1ed4ae5 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -8,8 +8,8 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; /** - * Tests @PolyModifiable, which expands to @PolyGrow @PolyShrink @PolyReplace and thus preserves all - * three capabilities independently. + * Tests @PolyModifiable, which expands to @PolyGrowable @PolyShrinkable @PolyReplaceable and thus + * preserves all three capabilities independently. */ public class PolyModifiableTest { @@ -20,9 +20,9 @@ public class PolyModifiableTest { void testPoly( @Modifiable List mod, - @Growable @Shrinkable List gs, // Grow+Shrink; Replace = MaybeReplace (default) - @Growable @Replaceable List gr, // Grow+Replace; Shrink = MaybeShrink - @Shrinkable @Replaceable List sr, // Shrink+Replace; Grow = MaybeGrow + @Growable @Shrinkable List gs, // Grow+Shrink; Replace = MaybeReplaceable (default) + @Growable @Replaceable List gr, // Grow+Replace; Shrink = MaybeShrinkable + @Shrinkable @Replaceable List sr, // Shrink+Replace; Grow = MaybeGrowable @Growable List g, @Shrinkable List s, @Replaceable List r, @@ -36,7 +36,7 @@ void testPoly( @MaybeModifiable List m2 = identity(mod); // OK: G+S+R <: top in all // ============================================================ - // Identity on @Growable @Shrinkable (G+S, R=MaybeReplace) + // Identity on @Growable @Shrinkable (G+S, R=MaybeReplaceable) // ============================================================ @Growable @Shrinkable List gs1 = identity(gs); // OK // :: error: [assignment] @@ -47,7 +47,7 @@ void testPoly( @Replaceable List gs5 = identity(gs); // Error: MaybeR !<: Replaceable // ============================================================ - // Identity on @Growable @Replaceable (G+R, S=MaybeShrink) + // Identity on @Growable @Replaceable (G+R, S=MaybeShrinkable) // ============================================================ @Growable @Replaceable List gr1 = identity(gr); // OK // :: error: [assignment] @@ -58,7 +58,7 @@ void testPoly( @Replaceable List gr5 = identity(gr); // OK // ============================================================ - // Identity on @Shrinkable @Replaceable (S+R, G=MaybeGrow) + // Identity on @Shrinkable @Replaceable (S+R, G=MaybeGrowable) // ============================================================ @Shrinkable @Replaceable List sr1 = identity(sr); // OK // :: error: [assignment] diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkTest.java index 75a83ed11203..d831c0785c24 100644 --- a/checker/tests/modifiability/PolyShrinkTest.java +++ b/checker/tests/modifiability/PolyShrinkTest.java @@ -3,7 +3,7 @@ import java.util.Set; import org.checkerframework.checker.modifiability.qual.*; -public class PolyShrinkTest { +public class PolyShrinkableTest { void testMapKeySet() { // Modifiable map: keySet() returns @Shrinkable. Cannot add or replace. @@ -38,7 +38,7 @@ void testMapValues() { } void testMapEntrySet() { - // entrySet(): Set is @PolyShrink (cannot add), Entry is @PolyModifiable. + // entrySet(): Set is @PolyShrinkable (cannot add), Entry is @PolyModifiable. // Modifiable map: Set is @Shrinkable, Entry is @Modifiable. @Modifiable Map modMap = new java.util.HashMap<>(); diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 762a5dffb3c5..4a3e384eb631 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -3,9 +3,9 @@ import java.util.Queue; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.MaybeGrow; -import org.checkerframework.checker.modifiability.qual.MaybeReplace; -import org.checkerframework.checker.modifiability.qual.MaybeShrink; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; +import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; @@ -13,9 +13,10 @@ /** * Tests structural capability removal for Set, Queue, Map.Entry, and Iterator. * - *

Convenience aliases on Set / Queue lose the Replace capability (set Replace to @MaybeReplace). - * Convenience aliases on Map.Entry lose Grow and Shrink capabilities. Convenience aliases on - * Iterator lose Grow and Replace capabilities. Explicit capability annotations are preserved. + *

Convenience aliases on Set / Queue lose the Replace capability (set Replace + * to @MaybeReplaceable). Convenience aliases on Map.Entry lose Grow and Shrink capabilities. + * Convenience aliases on Iterator lose Grow and Replace capabilities. Explicit capability + * annotations are preserved. */ class SetTest { @@ -38,15 +39,15 @@ void testSetReplaceRemoved( } void testSetGrowShrinkPreserved( - @MaybeGrow @MaybeShrink @MaybeReplace Set unknown, + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Set unknown, @Growable Set growable, @Shrinkable Set shrinkable, @Growable @Shrinkable Set gs) { // Top in all hierarchies: accepts everything - @MaybeGrow @MaybeShrink @MaybeReplace Set u1 = growable; - @MaybeGrow @MaybeShrink @MaybeReplace Set u2 = shrinkable; - @MaybeGrow @MaybeShrink @MaybeReplace Set u3 = gs; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Set u1 = growable; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Set u2 = shrinkable; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Set u3 = gs; // @Growable Set: Grow=G, Shrink=Maybe, Replace=Maybe @Growable Set g1 = gs; // OK: G+S <: G @@ -75,8 +76,8 @@ void testQueueLikeSet( // ========================================================== void testEntryAssignment( - // No Replace bit: effective Replace = @MaybeReplace after G+S removal - Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry unknown, + // No Replace bit: effective Replace = @MaybeReplaceable after G+S removal + Map.@MaybeGrowable @MaybeShrinkable @MaybeReplaceable Entry unknown, Map.@Growable Entry growable, Map.@Shrinkable Entry shrinkable, Map.@Growable @Shrinkable Entry gs, @@ -87,11 +88,11 @@ void testEntryAssignment( Map.@Modifiable Entry modifiable, Map.@Growable @Shrinkable @Replaceable Entry explicit) { - // Group 1: entries without Replace bit become @MaybeGrow @MaybeShrink @MaybeReplace + // Group 1: entries without Replace bit become @MaybeGrowable @MaybeShrinkable @MaybeReplaceable // (G and S are removed → both become Maybe; R was already Maybe) - Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry u1 = growable; - Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry u2 = shrinkable; - Map.@MaybeGrow @MaybeShrink @MaybeReplace Entry u3 = gs; + Map.@MaybeGrowable @MaybeShrinkable @MaybeReplaceable Entry u1 = growable; + Map.@MaybeGrowable @MaybeShrinkable @MaybeReplaceable Entry u2 = shrinkable; + Map.@MaybeGrowable @MaybeShrinkable @MaybeReplaceable Entry u3 = gs; // Explicit Grow/Shrink annotations are preserved. // :: error: [assignment] Map.@Growable Entry g1 = unknown; @@ -99,9 +100,9 @@ void testEntryAssignment( Map.@Shrinkable Entry s1 = unknown; Map.@Growable Entry g2 = gs; // OK - Map.@MaybeGrow @MaybeShrink @Replaceable Entry r1 = growReplace; - Map.@MaybeGrow @MaybeShrink @Replaceable Entry r2 = shrinkReplace; - Map.@MaybeGrow @MaybeShrink @Replaceable Entry r3 = modifiable; + Map.@MaybeGrowable @MaybeShrinkable @Replaceable Entry r1 = growReplace; + Map.@MaybeGrowable @MaybeShrinkable @Replaceable Entry r2 = shrinkReplace; + Map.@MaybeGrowable @MaybeShrinkable @Replaceable Entry r3 = modifiable; // Explicit Grow/Shrink annotations are preserved on Map.Entry. // :: error: [assignment] Map.@Growable @Replaceable Entry gr1 = replaceable; // OK: same effective type @@ -113,8 +114,8 @@ void testEntryAssignment( Map.@Replaceable Entry gr2 = modifiable; Map.@Modifiable Entry m2 = explicit; - // Cross-group: Replace !<: MaybeReplace (wrong direction) is NOT an error — - // but MaybeReplace !<: Replaceable IS an error: + // Cross-group: Replace !<: MaybeReplaceable (wrong direction) is NOT an error — + // but MaybeReplaceable !<: Replaceable IS an error: // :: error: [assignment] Map.@Replaceable Entry bad1 = growable; // Error: R=Maybe !<: Replaceable } @@ -125,22 +126,23 @@ void testEntryAssignment( // ========================================================== void testIteratorAssignment( - // No Shrink bit: effective after G+R removal → @MaybeGrow @MaybeShrink @MaybeReplace - @MaybeGrow @MaybeShrink @MaybeReplace Iterator unknown, + // No Shrink bit: effective after G+R removal → @MaybeGrowable @MaybeShrinkable + // @MaybeReplaceable + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Iterator unknown, @Growable Iterator growable, @Replaceable Iterator replaceable, @Growable @Replaceable Iterator growReplace, - // Has Shrink bit: effective after G+R removal → @MaybeGrow @Shrinkable @MaybeReplace + // Has Shrink bit: effective after G+R removal → @MaybeGrowable @Shrinkable @MaybeReplaceable @Shrinkable Iterator shrinkable, @Growable @Shrinkable Iterator growShrink, @Shrinkable @Replaceable Iterator shrinkReplace, @Modifiable Iterator modifiable, @Growable @Shrinkable @Replaceable Iterator explicit) { - // Group 1: No Shrink bit → effective @MaybeGrow @MaybeShrink @MaybeReplace - @MaybeGrow @MaybeShrink @MaybeReplace Iterator u1 = growable; - @MaybeGrow @MaybeShrink @MaybeReplace Iterator u2 = replaceable; - @MaybeGrow @MaybeShrink @MaybeReplace Iterator u3 = growReplace; + // Group 1: No Shrink bit → effective @MaybeGrowable @MaybeShrinkable @MaybeReplaceable + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Iterator u1 = growable; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Iterator u2 = replaceable; + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Iterator u3 = growReplace; // Explicit Grow/Replace annotations are preserved. // :: error: [assignment] @Growable Iterator g1 = unknown; @@ -148,11 +150,11 @@ void testIteratorAssignment( // :: error: [assignment] @Growable @Replaceable Iterator gr1 = replaceable; - // Group 2: Has Shrink bit → effective @MaybeGrow @Shrinkable @MaybeReplace - @MaybeGrow @Shrinkable @MaybeReplace Iterator s1 = shrinkable; - @MaybeGrow @Shrinkable @MaybeReplace Iterator s2 = growShrink; - @MaybeGrow @Shrinkable @MaybeReplace Iterator s3 = shrinkReplace; - @MaybeGrow @Shrinkable @MaybeReplace Iterator s4 = modifiable; + // Group 2: Has Shrink bit → effective @MaybeGrowable @Shrinkable @MaybeReplaceable + @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator s1 = shrinkable; + @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator s2 = growShrink; + @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator s3 = shrinkReplace; + @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator s4 = modifiable; // Cross-assignments within group 2: @Shrinkable Iterator sh1 = modifiable; // :: error: [assignment] diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index d76519f72941..6af6edd209d8 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -85,11 +85,11 @@ \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @MaybeGrow @MaybeShrink @MaybeReplace @MaybeIteratorPolyMod + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyMod / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | - @BottomGrow @BottomShrink @BottomReplace @IteratorPolyMod + @BottomGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyMod \end{verbatim} \end{minipage} \end{smaller} @@ -101,10 +101,10 @@ Figure~\ref{fig-modifiability-lattice} shows the hierarchies. An assignment \code{T x = y} is valid only if the compile-time type of \code{y} is a subtype of (or equal to) \code{T} in every hierarchy simultaneously. For -example, \code{@Growable @MaybeShrink @MaybeReplace List} is a -subtype of \code{@MaybeGrow @MaybeShrink @MaybeReplace List} -(because \<@Growable> $\le$ \<@MaybeGrow>), but it is \emph{not} a -subtype of \code{@Ungrowable @MaybeShrink @MaybeReplace List} +example, \code{@Growable @MaybeShrinkable @MaybeReplaceable List} is a +subtype of \code{@MaybeGrowable @MaybeShrinkable @MaybeReplaceable List} +(because \<@Growable> $\le$ \<@MaybeGrowable>), but it is \emph{not} a +subtype of \code{@Ungrowable @MaybeShrinkable @MaybeReplaceable List} (because \<@Growable> and \<@Ungrowable> are incomparable siblings in the Grow hierarchy). @@ -130,7 +130,7 @@ at every invocation of a grow, shrink, or replace operation. \item[\refqualclass{checker/modifiability/qual}{MaybeModifiable}] -Alias for \code{@MaybeGrow @MaybeShrink @MaybeReplace}. +Alias for \code{@MaybeGrowable @MaybeShrinkable @MaybeReplaceable}. The checker cannot determine the modifiability of the collection. The checker conservatively issues a warning at every invocation of a grow, shrink, or replace operation. @@ -143,7 +143,7 @@ \code{List<@UnmodParam List>}. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] -Alias for \code{@PolyGrow @PolyShrink @PolyReplace} +Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable} (see Section~\ref{modifiability-polymorphic}). \end{description} @@ -162,7 +162,7 @@ Qualifiers in the Grow hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{MaybeGrow}] +\item[\refqualclass{checker/modifiability/qual}{MaybeGrowable}] The top qualifier in the Grow hierarchy. The checker cannot determine whether this collection supports grow operations. This is the default qualifier for unannotated types in the @@ -176,13 +176,13 @@ Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this collection will throw \UnsupportedOperationException. -\item[\refqualclass{checker/modifiability/qual}{BottomGrow}] +\item[\refqualclass{checker/modifiability/qual}{BottomGrowable}] The bottom qualifier in the Grow hierarchy. Programmers should rarely write it. \end{description} Qualifiers in the Shrink hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{MaybeShrink}] +\item[\refqualclass{checker/modifiability/qual}{MaybeShrinkable}] The top qualifier in the Shrink hierarchy. The checker cannot determine whether this collection supports shrink operations. This is the default qualifier for unannotated types in the @@ -197,13 +197,13 @@ Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on this collection will throw \UnsupportedOperationException. -\item[\refqualclass{checker/modifiability/qual}{BottomShrink}] +\item[\refqualclass{checker/modifiability/qual}{BottomShrinkable}] The bottom qualifier in the Shrink hierarchy. Programmers should rarely write it. \end{description} Qualifiers in the Replace hierarchy: \begin{description} -\item[\refqualclass{checker/modifiability/qual}{MaybeReplace}] +\item[\refqualclass{checker/modifiability/qual}{MaybeReplaceable}] The top qualifier in the Replace hierarchy. The checker cannot determine whether this collection supports replace operations. This is the default qualifier for unannotated types in the @@ -218,7 +218,7 @@ Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ on this collection will throw \UnsupportedOperationException. -\item[\refqualclass{checker/modifiability/qual}{BottomReplace}] +\item[\refqualclass{checker/modifiability/qual}{BottomReplaceable}] The bottom qualifier in the Replace hierarchy. Programmers should rarely write it. \end{description} @@ -227,11 +227,11 @@ \item[\refqualclass{checker/modifiability/qual}{MaybeIteratorPolyMod}] The top qualifier. The checker cannot determine whether this collection's \code{iterator()} is -\<@PolyShrink>. +\<@PolyShrinkable>. This is the default qualifier for unannotated types. \item[\refqualclass{checker/modifiability/qual}{IteratorPolyMod}] -This collection's \code{iterator()} is \<@PolyShrink>. That is, if +This collection's \code{iterator()} is \<@PolyShrinkable>. That is, if collection \ is \<@Shrinkable>, then \ is also \<@Shrinkable>. @@ -249,9 +249,9 @@ Because the three hierarchies are independent, subtyping checks each hierarchy individually. A \code{@Growable @Shrinkable List} is a -subtype of \code{@Growable List} (Shrink defaults to \<@MaybeShrink> on the +subtype of \code{@Growable List} (Shrink defaults to \<@MaybeShrinkable> on the right, Replace -defaults to \<@MaybeReplace> on both sides), but it is \emph{not} a +defaults to \<@MaybeReplaceable> on both sides), but it is \emph{not} a subtype of \code{@Growable @Shrinkable @Replaceable List} (due to the Replace qualifiers). @@ -263,25 +263,25 @@ \begin{description} -\item[\refqualclass{checker/modifiability/qual}{PolyGrow}] +\item[\refqualclass{checker/modifiability/qual}{PolyGrowable}] Polymorphic qualifier for the Grow hierarchy. The return type's growability matches the growability of whichever -formal parameter is annotated with \<@PolyGrow>. +formal parameter is annotated with \<@PolyGrowable>. -\item[\refqualclass{checker/modifiability/qual}{PolyShrink}] +\item[\refqualclass{checker/modifiability/qual}{PolyShrinkable}] Polymorphic qualifier for the Shrink hierarchy. The return type's Shrink qualifier matches the Shrink qualifier of -whichever formal parameter is annotated with \<@PolyShrink>. +whichever formal parameter is annotated with \<@PolyShrinkable>. Useful for methods such as \code{Map.keySet()} that preserve shrinkability while returning an \<@Ungrowable> view. -\item[\refqualclass{checker/modifiability/qual}{PolyReplace}] +\item[\refqualclass{checker/modifiability/qual}{PolyReplaceable}] Polymorphic qualifier for the Replace hierarchy. The return type's replaceability matches the replaceability of -whichever formal parameter is annotated with \<@PolyReplace>. +whichever formal parameter is annotated with \<@PolyReplaceable>. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] -Alias for \code{@PolyGrow @PolyShrink @PolyReplace}. +Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable}. Preserves all three capabilities simultaneously. Use on methods such as \code{Collections.synchronizedList} that are pure wrappers and do not change modifiability. @@ -293,18 +293,18 @@ polymorphic and concrete qualifiers across hierarchies. For example: \begin{Verbatim} - @Ungrowable @PolyShrink Set keySet(@PolyShrink Map map) + @Ungrowable @PolyShrinkable Set keySet(@PolyShrinkable Map map) \end{Verbatim} If the map is \code{@Growable @Shrinkable @Replaceable}, then \code{keySet} returns -\code{@Ungrowable @Shrinkable @MaybeReplace Set}. +\code{@Ungrowable @Shrinkable @MaybeReplaceable Set}. -If the map is \code{@MaybeGrow @MaybeShrink @MaybeReplace}, then -\code{keySet} returns \code{@Ungrowable @MaybeShrink @MaybeReplace Set}. +If the map is \code{@MaybeGrowable @MaybeShrinkable @MaybeReplaceable}, then +\code{keySet} returns \code{@Ungrowable @MaybeShrinkable @MaybeReplaceable Set}. If the map is \code{@Ungrowable @Unshrinkable @Unreplaceable}, then -\code{keySet} returns \code{@Ungrowable @Unshrinkable @MaybeReplace Set}. +\code{keySet} returns \code{@Ungrowable @Unshrinkable @MaybeReplaceable Set}. \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} @@ -334,7 +334,7 @@ When a programmer writes \<@Modifiable> or \<@Unmodifiable> on a type that has no method for a given mutation, the Modifiability Checker uses the top qualifier. For example, both \<@Modifiable Queue> and \<@Unmodifiable -Queue> have \<@MaybeReplace> as their replaceability qualifier. (In the +Queue> have \<@MaybeReplaceable> as their replaceability qualifier. (In the above assignment, \ was flow-sensitively refined to have type \<@Replaceable>.) Other examples are that \ has no replace methods and \ has no grow or replace methods. @@ -362,7 +362,7 @@ \ is \begin{Verbatim} -@PolyShrink Iterator iterator(@PolyShrink MySubclassOfCollection this) +@PolyShrinkable Iterator iterator(@PolyShrinkable MySubclassOfCollection this) \end{Verbatim} A collection uses the second signature if the collection is @@ -426,7 +426,7 @@ map.put("key", "value"); // OK // Map.remove requires Shrink capability - map.remove("key"); // Error: @MaybeShrink (default) !<: @Shrinkable + map.remove("key"); // Error: @MaybeShrinkable (default) !<: @Shrinkable } // @PolyModifiable preserves all three capabilities @@ -438,7 +438,7 @@ @Modifiable List m = wrap(mod); // OK @Growable @Shrinkable List gs2 = wrap(gs); // OK // :: error: [assignment] - @Modifiable List bad = wrap(gs); // Error: gs has @MaybeReplace + @Modifiable List bad = wrap(gs); // Error: gs has @MaybeReplaceable } } \end{Verbatim} @@ -446,10 +446,10 @@ % LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap % LocalWords: toArray addAll removeAll retainAll replaceAll setValue -% LocalWords: MaybeModifiable UnmodParam Growable MaybeGrow MaybeShrink -% LocalWords: MaybeReplace growable ListIterator PolyModifiable addFirst -% LocalWords: synchronizedList PolyShrink PolyGrow PolyReplace ThrowsUOE +% LocalWords: MaybeModifiable UnmodParam Growable MaybeGrowable MaybeShrinkable +% LocalWords: MaybeReplaceable growable ListIterator PolyModifiable addFirst +% LocalWords: synchronizedList PolyShrinkable PolyGrowable PolyReplaceable ThrowsUOE % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable -% LocalWords: Unreplaceable BottomGrow BottomShrink BottomReplace +% LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable % LocalWords: MaybeIteratorPolyMod IteratorPolyMod From f7a19dd75e8b6c42f40c541b8cf10cce5e85787e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 24 May 2026 02:56:15 -0700 Subject: [PATCH 178/308] Rename file --- .../{PolyShrinkTest.java => PolyShrinkableTest.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename checker/tests/modifiability/{PolyShrinkTest.java => PolyShrinkableTest.java} (100%) diff --git a/checker/tests/modifiability/PolyShrinkTest.java b/checker/tests/modifiability/PolyShrinkableTest.java similarity index 100% rename from checker/tests/modifiability/PolyShrinkTest.java rename to checker/tests/modifiability/PolyShrinkableTest.java From 4afa7a6d73836df941eaae67d08a369381a4dffd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 24 May 2026 03:16:46 -0700 Subject: [PATCH 179/308] Expand `@UnmodParam` => `@UnmodifiableParam` --- buildSrc/src/main/groovy/cf-spotless.gradle | 2 +- ...UnmodParam.java => UnmodifiableParam.java} | 2 +- .../modifiability/ModifiabilityVisitor.java | 27 ++++++++++--------- .../grow/GrowAnnotatedTypeFactory.java | 4 +-- .../replace/ReplaceAnnotatedTypeFactory.java | 4 +-- .../modifiability/replace/ReplaceVisitor.java | 5 ++-- .../shrink/ShrinkAnnotatedTypeFactory.java | 4 +-- .../modifiability/shrink/ShrinkVisitor.java | 5 ++-- .../checker/modifiability/messages.properties | 2 +- .../modifiability/BasicModifiabilityTest.java | 4 +-- .../modifiability/UnmodParamLocationTest.java | 26 +++++++++--------- docs/manual/modifiability-checker.tex | 8 +++--- 12 files changed, 48 insertions(+), 45 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{UnmodParam.java => UnmodifiableParam.java} (96%) diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index b3c6a526401d..ac83e54b0f7b 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -111,7 +111,7 @@ spotless { .addTypeAnnotation("MaybeReplaceable") .addTypeAnnotation("MaybeShrinkable") .addTypeAnnotation("Unmodifiable") - .addTypeAnnotation("UnmodParam") + .addTypeAnnotation("UnmodifiableParam") .addTypeAnnotation("Unreplaceable") .addTypeAnnotation("Unshrinkable") } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java index 7bde3f044d28..3f3662d5a767 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodParam.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java @@ -20,4 +20,4 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -public @interface UnmodParam {} +public @interface UnmodifiableParam {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index aa8832b16363..7a768287dce5 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -12,7 +12,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.qual.ThrowsUOE; -import org.checkerframework.checker.modifiability.qual.UnmodParam; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -75,8 +75,8 @@ public void processClassTree(ClassTree classTree) { @Override public Void visitAnnotation(AnnotationTree tree, Void p) { - if (shouldCheckUnmodParamLocation() && isUnmodParamAnnotation(tree)) { - if (!isWithinAllowedUnmodParamLocation()) { + if (shouldCheckUnmodifiableParamLocation() && isUnmodifiableParamAnnotation(tree)) { + if (!isWithinAllowedUnmodifiableParamLocation()) { checker.reportError(tree, "unmodparam.location"); } } @@ -114,14 +114,14 @@ protected boolean shouldCheckThrowsUOE() { } /** - * Returns true if this checker should report {@code @UnmodParam} location errors. + * Returns true if this checker should report {@code @UnmodifiableParam} location errors. * *

The default is {@code true}. Shrink and Replace override this to avoid tripling diagnostics * when running under the aggregate {@link ModifiabilityChecker}. * - * @return true if this visitor should report {@code @UnmodParam} location errors + * @return true if this visitor should report {@code @UnmodifiableParam} location errors */ - protected boolean shouldCheckUnmodParamLocation() { + protected boolean shouldCheckUnmodifiableParamLocation() { return true; } @@ -139,30 +139,31 @@ protected boolean shouldCheckCustomModifiabilityAnnotation() { } /** - * Returns true if {@code tree} is an {@link UnmodParam} annotation. + * Returns true if {@code tree} is an {@link UnmodifiableParam} annotation. * * @param tree an annotation tree - * @return true if {@code tree} is an {@code @UnmodParam} annotation + * @return true if {@code tree} is an {@code @UnmodifiableParam} annotation */ - private boolean isUnmodParamAnnotation(AnnotationTree tree) { + private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { String annotationName = tree.getAnnotationType().toString(); // Quick check to avoid expensive annotation resolution for most annotations. - if (!annotationName.equals("UnmodParam") && !annotationName.endsWith(".UnmodParam")) { + if (!annotationName.equals("UnmodifiableParam") + && !annotationName.endsWith(".UnmodifiableParam")) { return false; } AnnotationMirror annotation = TreeUtils.annotationFromAnnotationTree(tree); - return annotation != null && atypeFactory.areSameByClass(annotation, UnmodParam.class); + return annotation != null && atypeFactory.areSameByClass(annotation, UnmodifiableParam.class); } /** * Returns true if the current annotation path is inside a method/constructor parameter type or * explicit receiver parameter type. * - * @return true if {@code @UnmodParam} is allowed at the current location + * @return true if {@code @UnmodifiableParam} is allowed at the current location */ @SuppressWarnings("interning:not.interned") // AST node comparison - private boolean isWithinAllowedUnmodParamLocation() { + private boolean isWithinAllowedUnmodifiableParamLocation() { // Find the declaration that contains the annotation, if any. TreePath path = getCurrentPath(); TreePath variablePath = null; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 679e46918be3..164351ed6bf7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -22,8 +22,8 @@ import org.checkerframework.checker.modifiability.qual.PolyGrowable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Ungrowable; -import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -125,7 +125,7 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, Unmodifiable.class)) { return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : UNGROWABLE; } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodParam.class)) { + || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_GROW; } else if (areSameByClass(annotation, PolyModifiable.class)) { return tm != null && isMapEntry(tm) ? MAYBE_GROW : POLY_GROW; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index cd6b2d0c0f82..405874e70cad 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -21,8 +21,8 @@ import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplaceable; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -140,7 +140,7 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, Unmodifiable.class)) { return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : UNREPLACEABLE; } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodParam.class)) { + || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_REPLACE; } else if (areSameByClass(annotation, PolyModifiable.class)) { return POLY_REPLACE; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index a18d5ec13ad3..ec3979311ca7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -22,8 +22,9 @@ protected boolean shouldCheckThrowsUOE() { } @Override - protected boolean shouldCheckUnmodParamLocation() { - // When running under ModifiabilityChecker, GrowChecker handles @UnmodParam location reporting. + protected boolean shouldCheckUnmodifiableParamLocation() { + // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location + // reporting. return checker.getParentChecker() == null; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 8e6092cffc7c..dec77a659a6e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -21,8 +21,8 @@ import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -122,7 +122,7 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, Unmodifiable.class)) { return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : UNSHRINKABLE; } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodParam.class)) { + || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_SHRINK; } else if (areSameByClass(annotation, PolyModifiable.class)) { return tm != null && isMapEntry(tm) ? MAYBE_SHRINK : POLY_SHRINK; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index bef513d1944b..a723a3b88138 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -22,8 +22,9 @@ protected boolean shouldCheckThrowsUOE() { } @Override - protected boolean shouldCheckUnmodParamLocation() { - // When running under ModifiabilityChecker, GrowChecker handles @UnmodParam location reporting. + protected boolean shouldCheckUnmodifiableParamLocation() { + // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location + // reporting. return checker.getParentChecker() == null; } diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index 99302158fae8..194399bf4a2b 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1,3 +1,3 @@ usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. -unmodparam.location=@UnmodParam may only be written on method/constructor parameters or explicit receiver parameters. +unmodparam.location=@UnmodifiableParam may only be written on method/constructor parameters or explicit receiver parameters. modifiability.annotation.unverified=Modifiability annotations/defaults on source-defined collection/map/iterator type %s are trusted but not verified by the checker; manually verify them and suppress this warning if correct. diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 269ed6241c2e..90e4f6352b9d 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -4,8 +4,8 @@ import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.UnmodParam; import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; // this checkes the correctness of annotation in List.java and ONLY the initialization of ArrayList public class BasicModifiabilityTest { @@ -102,7 +102,7 @@ void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List unmod.retainAll(other); } - void testUnmodParamMutation(@UnmodParam List unmodParam, String value) { + void testUnmodifiableParamMutation(@UnmodifiableParam List unmodParam, String value) { // :: error: [method.invocation] unmodParam.add(value); // :: error: [method.invocation] diff --git a/checker/tests/modifiability/UnmodParamLocationTest.java b/checker/tests/modifiability/UnmodParamLocationTest.java index 35a8fac47710..6a7ab643bda7 100644 --- a/checker/tests/modifiability/UnmodParamLocationTest.java +++ b/checker/tests/modifiability/UnmodParamLocationTest.java @@ -1,47 +1,47 @@ import java.util.List; import java.util.function.Consumer; -import org.checkerframework.checker.modifiability.qual.UnmodParam; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; class UnmodParamLocationTest< // :: error: [unmodparam.location] - @UnmodParam T> { + @UnmodifiableParam T> { // :: error: [unmodparam.location] - @UnmodParam List field; + @UnmodifiableParam List field; // :: error: [unmodparam.location] - List<@UnmodParam List> nestedField; + List<@UnmodifiableParam List> nestedField; - UnmodParamLocationTest(@UnmodParam List parameter) {} + UnmodParamLocationTest(@UnmodifiableParam List parameter) {} - void method(@UnmodParam List parameter) {} + void method(@UnmodifiableParam List parameter) {} - void nestedParameter(List<@UnmodParam List> parameter) {} + void nestedParameter(List<@UnmodifiableParam List> parameter) {} - void receiver(@UnmodParam UnmodParamLocationTest this) {} + void receiver(@UnmodifiableParam UnmodParamLocationTest this) {} // :: error: [unmodparam.location] - @UnmodParam List returnType() { + @UnmodifiableParam List returnType() { return null; } @SuppressWarnings("unchecked") void local(Object object) { // :: error: [unmodparam.location] - @UnmodParam List local = null; + @UnmodifiableParam List local = null; local = null; // :: error: [unmodparam.location] - List cast = (@UnmodParam List) object; + List cast = (@UnmodifiableParam List) object; cast = null; } void lambda() { // :: error: [unmodparam.location] - Consumer> consumer = (@UnmodParam List parameter) -> {}; + Consumer> consumer = (@UnmodifiableParam List parameter) -> {}; consumer.accept(null); } // :: error: [unmodparam.location] - <@UnmodParam S> void methodTypeParameter(S value) {} + <@UnmodifiableParam S> void methodTypeParameter(S value) {} } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 6af6edd209d8..808014e8ef4b 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -135,12 +135,12 @@ The checker conservatively issues a warning at every invocation of a grow, shrink, or replace operation. -\item[\refqualclass{checker/modifiability/qual}{UnmodParam}] +\item[\refqualclass{checker/modifiability/qual}{UnmodifiableParam}] A syntactic sugar for \refqualclass{checker/modifiability/qual}{MaybeModifiable}. It may only be written within a method or constructor parameter type, or within an explicit receiver parameter type, when the method is not designed to change the parameter. It may appear nested within such a parameter type, such as in -\code{List<@UnmodParam List>}. +\code{List<@UnmodifiableParam List>}. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable} @@ -155,7 +155,7 @@ @Modifiable List mod = new ArrayList<>(); // all three capabilities @Unmodifiable List unmod = List.of("a"); // no capabilities @MaybeModifiable List unknown = ...; // capabilities are unknown at compile time - void foo(@UnmodParam List list) { ... } // foo does not modify list + void foo(@UnmodifiableParam List list) { ... } // foo does not modify list \end{Verbatim} \subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} @@ -446,7 +446,7 @@ % LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap % LocalWords: toArray addAll removeAll retainAll replaceAll setValue -% LocalWords: MaybeModifiable UnmodParam Growable MaybeGrowable MaybeShrinkable +% LocalWords: MaybeModifiable UnmodifiableParam Growable MaybeGrowable MaybeShrinkable % LocalWords: MaybeReplaceable growable ListIterator PolyModifiable addFirst % LocalWords: synchronizedList PolyShrinkable PolyGrowable PolyReplaceable ThrowsUOE % LocalWords: addLast unreplaceable Growability shrinkability replacability From 1bf79507efc7be125d2b5f5c74b098336167adf1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 24 May 2026 23:21:22 -0700 Subject: [PATCH 180/308] fixed getOrDefaultTest.java and add error comments. --- .../tests/modifiability/GetOrDefaultTest.java | 272 ++++++++++++------ 1 file changed, 178 insertions(+), 94 deletions(-) diff --git a/checker/tests/modifiability/GetOrDefaultTest.java b/checker/tests/modifiability/GetOrDefaultTest.java index 32dccc5efea6..25fca75fcd6b 100644 --- a/checker/tests/modifiability/GetOrDefaultTest.java +++ b/checker/tests/modifiability/GetOrDefaultTest.java @@ -1,4 +1,4 @@ -import java.util.Date; +import java.util.List; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -16,115 +16,199 @@ interface MyGodMap { public class GetOrDefaultTest { - Date d; - @Modifiable Date dm; - @Unmodifiable Date du; + List d; + @Modifiable List dm; + @Unmodifiable List du; - void foo1(MyGodMap m) { - Date d1 = m.getOrDefault("hello", d); - @Modifiable Date d2 = m.getOrDefault("hello", d); - @Unmodifiable Date d3 = m.getOrDefault("hello", d); - Date d4 = m.getOrDefault("hello", dm); - @Modifiable Date d5 = m.getOrDefault("hello", dm); - @Unmodifiable Date d6 = m.getOrDefault("hello", dm); - Date d7 = m.getOrDefault("hello", du); - @Modifiable Date d8 = m.getOrDefault("hello", du); - @Unmodifiable Date d9 = m.getOrDefault("hello", du); + void foo1(MyGodMap> m) { + List d1 = m.getOrDefault("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d2 = m.getOrDefault("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d3 = m.getOrDefault("hello", d); + List d4 = m.getOrDefault("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d5 = m.getOrDefault("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefault("hello", dm); + List d7 = m.getOrDefault("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = m.getOrDefault("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d9 = m.getOrDefault("hello", du); } - void foo2(MyGodMap m) { - Date d1 = m.getOrDefault("hello", d); - @Modifiable Date d2 = m.getOrDefault("hello", d); - @Unmodifiable Date d3 = m.getOrDefault("hello", d); - Date d4 = m.getOrDefault("hello", dm); - @Modifiable Date d5 = m.getOrDefault("hello", dm); - @Unmodifiable Date d6 = m.getOrDefault("hello", dm); - Date d7 = m.getOrDefault("hello", du); - @Modifiable Date d8 = m.getOrDefault("hello", du); - @Unmodifiable Date d9 = m.getOrDefault("hello", du); + void foo2(MyGodMap> m) { + // :: error: [argument] :: error: [argument] :: error: [argument] + List d1 = m.getOrDefault("hello", d); + // :: error: [argument] :: error: [argument] :: error: [argument] + @Modifiable List d2 = m.getOrDefault("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + // :: error: [argument] :: error: [argument] :: error: [argument] + @Unmodifiable List d3 = m.getOrDefault("hello", d); + List d4 = m.getOrDefault("hello", dm); + @Modifiable List d5 = m.getOrDefault("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefault("hello", dm); + // :: error: [argument] :: error: [argument] :: error: [argument] + List d7 = m.getOrDefault("hello", du); + // :: error: [argument] :: error: [argument] :: error: [argument] + @Modifiable List d8 = m.getOrDefault("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + // :: error: [argument] :: error: [argument] :: error: [argument] + @Unmodifiable List d9 = m.getOrDefault("hello", du); } - void foo3(MyGodMap m) { - Date d1 = m.getOrDefault("hello", d); - @Modifiable Date d2 = m.getOrDefault("hello", d); - @Unmodifiable Date d3 = m.getOrDefault("hello", d); - Date d4 = m.getOrDefault("hello", dm); - @Modifiable Date d5 = m.getOrDefault("hello", dm); - @Unmodifiable Date d6 = m.getOrDefault("hello", dm); - Date d7 = m.getOrDefault("hello", du); - @Modifiable Date d8 = m.getOrDefault("hello", du); - @Unmodifiable Date d9 = m.getOrDefault("hello", du); + void foo3(MyGodMap> m) { + // :: error: [argument] :: error: [argument] :: error: [argument] + List d1 = m.getOrDefault("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + // :: error: [argument] :: error: [argument] :: error: [argument] + @Modifiable List d2 = m.getOrDefault("hello", d); + // :: error: [argument] :: error: [argument] :: error: [argument] + @Unmodifiable List d3 = m.getOrDefault("hello", d); + // :: error: [argument] :: error: [argument] :: error: [argument] + List d4 = m.getOrDefault("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + // :: error: [argument] :: error: [argument] :: error: [argument] + @Modifiable List d5 = m.getOrDefault("hello", dm); + // :: error: [argument] :: error: [argument] :: error: [argument] + @Unmodifiable List d6 = m.getOrDefault("hello", dm); + List d7 = m.getOrDefault("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = m.getOrDefault("hello", du); + @Unmodifiable List d9 = m.getOrDefault("hello", du); } - void foo4(MyGodMap m) { - Date d1 = m.getOrDefaultPoly("hello", d); - @Modifiable Date d2 = m.getOrDefaultPoly("hello", d); - @Unmodifiable Date d3 = m.getOrDefaultPoly("hello", d); - Date d4 = m.getOrDefaultPoly("hello", dm); - @Modifiable Date d5 = m.getOrDefaultPoly("hello", dm); - @Unmodifiable Date d6 = m.getOrDefaultPoly("hello", dm); - Date d7 = m.getOrDefaultPoly("hello", du); - @Modifiable Date d8 = m.getOrDefaultPoly("hello", du); - @Unmodifiable Date d9 = m.getOrDefaultPoly("hello", du); + void foo4(MyGodMap> m) { + List d1 = m.getOrDefaultPoly("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d2 = m.getOrDefaultPoly("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d3 = m.getOrDefaultPoly("hello", d); + List d4 = m.getOrDefaultPoly("hello", dm); + @Modifiable List d5 = m.getOrDefaultPoly("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefaultPoly("hello", dm); + List d7 = m.getOrDefaultPoly("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = m.getOrDefaultPoly("hello", du); + @Unmodifiable List d9 = m.getOrDefaultPoly("hello", du); } - void foo5(MyGodMap m) { - Date d1 = m.getOrDefaultPoly("hello", d); - @Modifiable Date d2 = m.getOrDefaultPoly("hello", d); - @Unmodifiable Date d3 = m.getOrDefaultPoly("hello", d); - Date d4 = m.getOrDefaultPoly("hello", dm); - @Modifiable Date d5 = m.getOrDefaultPoly("hello", dm); - @Unmodifiable Date d6 = m.getOrDefaultPoly("hello", dm); - Date d7 = m.getOrDefaultPoly("hello", du); - @Modifiable Date d8 = m.getOrDefaultPoly("hello", du); - @Unmodifiable Date d9 = m.getOrDefaultPoly("hello", du); + void foo5(MyGodMap> m) { + List d1 = m.getOrDefaultPoly("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d2 = m.getOrDefaultPoly("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d3 = m.getOrDefaultPoly("hello", d); + List d4 = m.getOrDefaultPoly("hello", dm); + @Modifiable List d5 = m.getOrDefaultPoly("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefaultPoly("hello", dm); + List d7 = m.getOrDefaultPoly("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = m.getOrDefaultPoly("hello", du); + @Unmodifiable List d9 = m.getOrDefaultPoly("hello", du); } - void foo6(MyGodMap m) { - Date d1 = m.getOrDefaultPoly("hello", d); - @Modifiable Date d2 = m.getOrDefaultPoly("hello", d); - @Unmodifiable Date d3 = m.getOrDefaultPoly("hello", d); - Date d4 = m.getOrDefaultPoly("hello", dm); - @Modifiable Date d5 = m.getOrDefaultPoly("hello", dm); - @Unmodifiable Date d6 = m.getOrDefaultPoly("hello", dm); - Date d7 = m.getOrDefaultPoly("hello", du); - @Modifiable Date d8 = m.getOrDefaultPoly("hello", du); - @Unmodifiable Date d9 = m.getOrDefaultPoly("hello", du); + void foo6(MyGodMap> m) { + List d1 = m.getOrDefaultPoly("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d2 = m.getOrDefaultPoly("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d3 = m.getOrDefaultPoly("hello", d); + List d4 = m.getOrDefaultPoly("hello", dm); + @Modifiable List d5 = m.getOrDefaultPoly("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefaultPoly("hello", dm); + List d7 = m.getOrDefaultPoly("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = m.getOrDefaultPoly("hello", du); + @Unmodifiable List d9 = m.getOrDefaultPoly("hello", du); } - void foo7(MyGodMap m) { - Date d1 = m.getOrDefault2("hello", d); - @Modifiable Date d2 = m.getOrDefault2("hello", d); - @Unmodifiable Date d3 = m.getOrDefault2("hello", d); - Date d4 = m.getOrDefault2("hello", dm); - @Modifiable Date d5 = m.getOrDefault2("hello", dm); - @Unmodifiable Date d6 = m.getOrDefault2("hello", dm); - Date d7 = m.getOrDefault2("hello", du); - @Modifiable Date d8 = m.getOrDefault2("hello", du); - @Unmodifiable Date d9 = m.getOrDefault2("hello", du); + void foo7(MyGodMap> m) { + List d1 = m.getOrDefault2("hello", d); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Modifiable List d2 = m.getOrDefault2("hello", d); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d3 = m.getOrDefault2("hello", d); + List d4 = m.getOrDefault2("hello", dm); + @Modifiable List d5 = m.getOrDefault2("hello", dm); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d6 = m.getOrDefault2("hello", dm); + List d7 = m.getOrDefault2("hello", du); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Modifiable List d8 = m.getOrDefault2("hello", du); + @Unmodifiable List d9 = m.getOrDefault2("hello", du); } - void foo8(MyGodMap m) { - Date d1 = m.getOrDefault2("hello", d); - @Modifiable Date d2 = m.getOrDefault2("hello", d); - @Unmodifiable Date d3 = m.getOrDefault2("hello", d); - Date d4 = m.getOrDefault2("hello", dm); - @Modifiable Date d5 = m.getOrDefault2("hello", dm); - @Unmodifiable Date d6 = m.getOrDefault2("hello", dm); - Date d7 = m.getOrDefault2("hello", du); - @Modifiable Date d8 = m.getOrDefault2("hello", du); - @Unmodifiable Date d9 = m.getOrDefault2("hello", du); + void foo8(MyGodMap> m) { + // :: error: [type.arguments.not.inferred] :: error: [type.arguments.not.inferred] :: error: + // [type.arguments.not.inferred] + List d1 = m.getOrDefault2("hello", d); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Modifiable List d2 = m.getOrDefault2("hello", d); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d3 = m.getOrDefault2("hello", d); + List d4 = m.getOrDefault2("hello", dm); + @Modifiable List d5 = m.getOrDefault2("hello", dm); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d6 = m.getOrDefault2("hello", dm); + // :: error: [type.arguments.not.inferred] :: error: [type.arguments.not.inferred] :: error: + // [type.arguments.not.inferred] + List d7 = m.getOrDefault2("hello", du); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Modifiable List d8 = m.getOrDefault2("hello", du); + // :: error: [type.arguments.not.inferred] :: error: [type.arguments.not.inferred] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d9 = m.getOrDefault2("hello", du); } - void foo9(MyGodMap m) { - Date d1 = m.getOrDefault2("hello", d); - @Modifiable Date d2 = m.getOrDefault2("hello", d); - @Unmodifiable Date d3 = m.getOrDefault2("hello", d); - Date d4 = m.getOrDefault2("hello", dm); - @Modifiable Date d5 = m.getOrDefault2("hello", dm); - @Unmodifiable Date d6 = m.getOrDefault2("hello", dm); - Date d7 = m.getOrDefault2("hello", du); - @Modifiable Date d8 = m.getOrDefault2("hello", du); - @Unmodifiable Date d9 = m.getOrDefault2("hello", du); + void foo9(MyGodMap> m) { + // :: error: [type.arguments.not.inferred] :: error: [type.arguments.not.inferred] :: error: + // [type.arguments.not.inferred] + List d1 = m.getOrDefault2("hello", d); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Modifiable List d2 = m.getOrDefault2("hello", d); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d3 = m.getOrDefault2("hello", d); + // :: error: [type.arguments.not.inferred] :: error: [type.arguments.not.inferred] :: error: + // [type.arguments.not.inferred] + List d4 = m.getOrDefault2("hello", dm); + // :: error: [type.arguments.not.inferred] :: error: [type.arguments.not.inferred] :: error: + // [type.arguments.not.inferred] + @Modifiable List d5 = m.getOrDefault2("hello", dm); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Unmodifiable List d6 = m.getOrDefault2("hello", dm); + List d7 = m.getOrDefault2("hello", du); + // :: error: [assignment] :: error: [type.arguments.not.inferred] :: error: [assignment] :: + // error: [type.arguments.not.inferred] :: error: [assignment] :: error: + // [type.arguments.not.inferred] + @Modifiable List d8 = m.getOrDefault2("hello", du); + @Unmodifiable List d9 = m.getOrDefault2("hello", du); } } From 6be0ca4d522df0e48d8ebe0bb2a68265cbde8881 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 25 May 2026 12:30:25 -0700 Subject: [PATCH 181/308] Explain purpose of command-line argument --- checker/bin-devel/count-suppression-reasons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/bin-devel/count-suppression-reasons b/checker/bin-devel/count-suppression-reasons index 19a060e0ee2b..89884f74a481 100755 --- a/checker/bin-devel/count-suppression-reasons +++ b/checker/bin-devel/count-suppression-reasons @@ -9,7 +9,7 @@ # -d debug # -i REGEX include matches for the grep basic regex # -x REGEX exclude matches for the grep basic regex -# -y REGEX exclude matches for the grep basic regex +# -y REGEX exclude matches for the grep basic regex (enables supplying two regexes) # The -i argument (if any) is processed before the -x argument (if any). # The "reason" for a warning suppression is the Java line comment after it: From a69b9ac9e780d8e5ef2bfe01d448578e613f8e8f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 26 May 2026 15:57:33 -0700 Subject: [PATCH 182/308] Tweak --- checker/tests/modifiability/BasicModifiableTest3.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/tests/modifiability/BasicModifiableTest3.java b/checker/tests/modifiability/BasicModifiableTest3.java index 69add9bccbb9..1fd018b252fa 100644 --- a/checker/tests/modifiability/BasicModifiableTest3.java +++ b/checker/tests/modifiability/BasicModifiableTest3.java @@ -7,7 +7,7 @@ public class BasicModifiableTest3 { /* @ReleasesNoLocks - @SideEffectsOnly("this") + // @SideEffectsOnly("this") @EnsuresNonEmpty("this") */ boolean my_add(@Modifiable @GuardSatisfied List this_one, E e) { From 9ed50c67c629baf79f53534806bfae5b2a020287 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 28 May 2026 17:10:52 -0700 Subject: [PATCH 183/308] Fix `@RelevantJavaTypes` --- .../checker/modifiability/grow/GrowChecker.java | 9 +-------- .../checker/modifiability/replace/ReplaceChecker.java | 9 +-------- .../checker/modifiability/shrink/ShrinkChecker.java | 9 +-------- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index c4095808193b..a55d40c66e9b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability.grow; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; @@ -19,13 +18,7 @@ * * @checker_framework.manual #modifiability-checker Modifiability Checker */ -@RelevantJavaTypes({ - Collection.class, - Iterator.class, - Map.class, - Map.Entry.class, - Collections.class -}) +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"growable", "modifiability"}) public class GrowChecker extends ModifiabilityBaseChecker { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index 273af4537da6..a9fe28262b34 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability.replace; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; @@ -19,13 +18,7 @@ * * @checker_framework.manual #modifiability-checker Modifiability Checker */ -@RelevantJavaTypes({ - Collection.class, - Iterator.class, - Map.class, - Map.Entry.class, - Collections.class -}) +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"replaceable", "modifiability"}) public class ReplaceChecker extends ModifiabilityBaseChecker { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index cafa8a471d87..31c48c6e7801 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability.shrink; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; @@ -19,13 +18,7 @@ * * @checker_framework.manual #modifiability-checker Modifiability Checker */ -@RelevantJavaTypes({ - Collection.class, - Iterator.class, - Map.class, - Map.Entry.class, - Collections.class -}) +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"shrinkable", "modifiability"}) public class ShrinkChecker extends ModifiabilityBaseChecker { From d57455ee7649ba5481a596c513b69927c8c0be14 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 28 May 2026 17:52:22 -0700 Subject: [PATCH 184/308] Use ASCII --- .../checker/modifiability/grow/GrowAnnotatedTypeFactory.java | 2 +- .../modifiability/replace/ReplaceAnnotatedTypeFactory.java | 2 +- .../modifiability/shrink/ShrinkAnnotatedTypeFactory.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 164351ed6bf7..746560737a08 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -46,7 +46,7 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.ListIterator} type. */ private final TypeMirror listIteratorErasure; - // ── Hierarchy qualifiers ────────── + // -- Hierarchy qualifiers ---------- /** The {@code @}{@link MaybeGrowable} qualifier (top of Grow hierarchy). */ private final AnnotationMirror MAYBE_GROW; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 405874e70cad..3e0bef0edcb7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -55,7 +55,7 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.ListIterator} type. */ private final TypeMirror listIteratorErasure; - // ── Hierarchy qualifiers ────────── + // -- Hierarchy qualifiers ---------- /** The {@code @}{@link MaybeReplaceable} qualifier (top of Replace hierarchy). */ private final AnnotationMirror MAYBE_REPLACE; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index dec77a659a6e..a6999c589abd 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -44,7 +44,7 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The erased {@code java.util.Iterator} type. */ private final TypeMirror iteratorErasure; - // ── Hierarchy qualifiers ────────── + // -- Hierarchy qualifiers ---------- /** The {@code @}{@link MaybeShrinkable} qualifier (top of Shrink hierarchy). */ private final AnnotationMirror MAYBE_SHRINK; From 9292fc2a0e84f2d2c738bf3445a2d493260c99d5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 29 May 2026 09:30:47 -0700 Subject: [PATCH 185/308] Code review changes --- .../afu/scenelib/util/coll/WrapperMap.java | 4 +- buildSrc/src/main/groovy/cf-spotless.gradle | 4 +- .../checker/modifiability/qual/Growable.java | 2 +- ...lyMod.java => IteratorPolyShrinkable.java} | 12 +- .../modifiability/qual/MaybeGrowable.java | 5 +- ....java => MaybeIteratorPolyShrinkable.java} | 6 +- .../modifiability/qual/MaybeReplaceable.java | 6 +- .../modifiability/qual/MaybeShrinkable.java | 7 +- .../modifiability/qual/Modifiable.java | 4 +- .../modifiability/qual/PolyModifiable.java | 10 +- .../modifiability/qual/Replaceable.java | 2 +- .../modifiability/qual/Shrinkable.java | 4 +- .../modifiability/qual/UnmodifiableParam.java | 12 +- .../modifiability/qual/Unshrinkable.java | 4 +- .../modifiability/ModifiabilityChecker.java | 2 +- .../ModifiabilityMethodUtils.java | 20 +- .../modifiability/ModifiabilityVisitor.java | 6 +- .../grow/GrowAnnotatedTypeFactory.java | 70 +++-- .../modifiability/grow/GrowChecker.java | 2 +- .../modifiability/grow/javaparser.astub | 4 +- .../replace/ReplaceAnnotatedTypeFactory.java | 68 +++-- .../modifiability/replace/javaparser.astub | 4 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 74 +++-- .../modifiability/shrink/javaparser.astub | 4 +- .../checker/modifiability/messages.properties | 2 +- .../modifiability/BasicModifiabilityTest.java | 4 +- .../modifiability/IteratorPrecisionTest.java | 18 +- docs/manual/modifiability-checker.tex | 254 +++++++++++------- 28 files changed, 329 insertions(+), 285 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{IteratorPolyMod.java => IteratorPolyShrinkable.java} (53%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{MaybeIteratorPolyMod.java => MaybeIteratorPolyShrinkable.java} (83%) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index dce3e04ee2c1..70734ddc7e81 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,7 +3,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; /** @@ -40,7 +40,7 @@ public boolean containsValue(Object value) { @SuppressWarnings("keyfor") // use of delegate object @Override - public @IteratorPolyMod @PolyShrinkable Set> entrySet( + public @IteratorPolyShrinkable @PolyShrinkable Set> entrySet( @PolyShrinkable WrapperMap this) { return back.entrySet(); } diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index ac83e54b0f7b..a41bb6f9ef8d 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -96,7 +96,7 @@ spotless { .addTypeAnnotation("BottomReplaceable") .addTypeAnnotation("BottomShrinkable") .addTypeAnnotation("Growable") - .addTypeAnnotation("IteratorPolyMod") + .addTypeAnnotation("IteratorPolyShrinkable") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrowable") .addTypeAnnotation("PolyModifiable") @@ -106,7 +106,7 @@ spotless { .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") .addTypeAnnotation("MaybeGrowable") - .addTypeAnnotation("MaybeIteratorPolyMod") + .addTypeAnnotation("MaybeIteratorPolyShrinkable") .addTypeAnnotation("MaybeModifiable") .addTypeAnnotation("MaybeReplaceable") .addTypeAnnotation("MaybeShrinkable") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java index 4167df7d64b9..4ad15fd4f8be 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Growable.java @@ -9,7 +9,7 @@ /** * Calling grow operations such as {@code add}, {@code addAll}, etc. on this collection will not - * result in throwing {@link UnsupportedOperationException}. + * throw {@link UnsupportedOperationException}. * *

No guarantees are made about shrink or replace operations. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrinkable.java similarity index 53% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrinkable.java index 8ad422162e21..0e41344e8a5f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrinkable.java @@ -8,10 +8,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} method preserves - * the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has type - * {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any - * collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is always + * If a collection's type is {@code @IteratorPolyShrinkable}, then its {@code iterator()} method + * preserves the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has + * type {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any + * collection whose type is {@code @MaybeIteratorPolyShrinkable}, its iterator is always * {@code @Unshrinkable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeIteratorPolyMod.class) -public @interface IteratorPolyMod {} +@SubtypeOf(MaybeIteratorPolyShrinkable.class) +public @interface IteratorPolyShrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java index 628c95dc13c7..c4914c496803 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.java @@ -9,8 +9,9 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Grow hierarchy. Represents unknown grow capability. The checker cannot - * determine whether the collection supports grow operations such as {@code add}. + * The top qualifier in the Grow hierarchy. The collection may or may not support grow operations + * such as {@code add}. The checker conservatively issues an error wherever a grow operation is + * called on a {@code @MaybeGrowable} expression. * *

This is the default qualifier for unannotated types in the Grow hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyShrinkable.java similarity index 83% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyShrinkable.java index 30f794ba7973..e48346a6d48d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyShrinkable.java @@ -9,11 +9,11 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * For the annotated collection, The return type of {@code iterator()} is {@code @Unshrinkable}. + * For the annotated collection, the return type of {@code iterator()} is {@code @Unshrinkable}. * *

This is the default qualifier for unannotated types. * - * @see IteratorPolyMod + * @see IteratorPolyShrinkable * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface MaybeIteratorPolyMod {} +public @interface MaybeIteratorPolyShrinkable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java index 641d51b83b56..1b1aa7978b95 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.java @@ -9,9 +9,9 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Replace hierarchy. Represents unknown replace capability. The checker - * cannot determine whether the collection supports replace operations such as {@code set} or {@code - * replaceAll}. + * The top qualifier in the Replace hierarchy. The collection may or may not support replace + * operations such as {@code set} and {@code replaceAll}. The checker conservatively issues an error + * wherever a replace operation is called on a {@code @MaybeReplaceable} expression. * *

This is the default qualifier for unannotated types in the Replace hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java index b6889579408b..2175b8951d5f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java @@ -9,9 +9,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Shrink hierarchy. Represents unknown shrink capability. The checker - * cannot determine whether the collection supports shrink operations such as {@code remove} or - * {@code clear}. + * The top qualifier in the Shrink hierarchy. The collection may or may not support shrink + * operations such as {@code remove}, {@code, removeAll}, and {@code clear}. The checker + * conservatively issues an error wherever a shrink operation is called on a + * {@code @MaybeShrinkable} expression. * *

This is the default qualifier for unannotated types in the Shrink hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index f02de1c0592c..6ea06c0065be 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -8,8 +8,8 @@ /** * Convenience alias usually meaning {@code @Growable @Shrinkable @Replaceable}. Calling a mutating - * operation (growing, shrinking, or replacing) on this collection will not result in throwing - * {@link UnsupportedOperationException}. + * operation (growing, shrinking, or replacing) on this collection will not throw {@link + * UnsupportedOperationException}. * *

As an exception, {@code @Modifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Modifiable Iterator} means diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 517762db7857..5176b1123f9d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -7,8 +7,8 @@ * Convenience alias meaning {@code @PolyGrowable @PolyReplaceable @PolyShrinkable}. A polymorphic * qualifier for all three modifiability hierarchies. * - *

You should write {@code @PolyModifiable} on methods that preserve or transfer modifiability, - * such as {@code List.subList()}, {@code iterator()}, and {@code stream()}. + *

Write {@code @PolyModifiable} on methods that preserve or transfer modifiability, such as + * {@code List.subList()} and {@code stream()}. * *

For example: * @@ -18,9 +18,9 @@ * } * * - * If the receiver type at a given call site is {@code @Unmodifiable}, the return type at that call - * site {@code @Unmodifiable}. If the receiver type is {@code @Modifiable}, the return type is - * {@code @Modifiable}. Likewise for growing and shrinking type qualifiers. + * At each call site, the return type is equal to the receiver type. If the receiver type is + * {@code @Growable}, the return type is {@code @Growable}. If the receiver type is + * {@code @Ungrowable}, the return type is {@code @Ungrowable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java index 9759df8a08b4..58a79dbd5b7a 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Replaceable.java @@ -9,7 +9,7 @@ /** * Calling replace operations such as {@code set}, {@code replaceAll}, etc. on this collection will - * not result in throwing {@link UnsupportedOperationException}. + * not throw {@link UnsupportedOperationException}. * *

No guarantees are made about grow or shrink operations. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java index 10af526c558c..39bb42266427 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Shrinkable.java @@ -8,8 +8,8 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * Calling shrink operations such as {@code remove}, {@code clear}, etc. on this collection will not - * result in throwing {@link UnsupportedOperationException}. + * Calling shrink operations such as {@code remove}, {@code removeAll}, {@code clear}, etc. on this + * collection will not throw {@link UnsupportedOperationException}. * *

No guarantees are made about grow or replace operations. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java index 3f3662d5a767..4c3390762cdc 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.java @@ -7,13 +7,13 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable}. This is a - * syntactic sugar for {@link MaybeModifiable}. It is intended to be used on parameters of methods - * that are not designed to change the parameter. This annotation is only allowed to be written - * within method or constructor parameter types, or explicit receiver parameter types. + * Convenience alias that is syntactic sugar for {@code @}{@link MaybeModifiable}. It may only be + * written within a formal parameter type. * - *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable} on each annotated type. + *

You can write {@code @UnmodifiableParam} to indicate that a method does not change its + * parameter. From the method's point of view, the parameter is unmodifiable. This is reference + * unmodifiability, because another alias can change the value, unlike {@code @}{@link Unmodifiable} + * which means object immutability: no alias can change the value. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java index c52ce9679d8a..eb32a3949516 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unshrinkable.java @@ -8,8 +8,8 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * Calling shrink operations such as {@code remove}, {@code clear}, etc. on this collection will - * throw {@link UnsupportedOperationException}. + * Calling shrink operations such as {@code remove}, {@code removeAll}, {@code clear}, etc. on this + * collection will throw {@link UnsupportedOperationException}. * *

No guarantees are made about grow or replace operations. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 131f502e1506..4ae41a814153 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -25,7 +25,7 @@ */ public class ModifiabilityChecker extends AggregateChecker { - /** Creates a Modifiability checker. */ + /** Creates a ModifiabilityChecker. */ public ModifiabilityChecker() {} @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java index d9720c39fc48..abc8e8cee8ff 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java @@ -21,7 +21,7 @@ private ModifiabilityMethodUtils() { } /** - * Returns true if {@code tree} invokes {@code + * Returns true if {@code tree} is a call to {@code * org.plumelib.util.CollectionsPlume.withoutDuplicates(Collection)}. * * @param tree a method invocation tree @@ -29,16 +29,14 @@ private ModifiabilityMethodUtils() { */ public static boolean isCollectionsPlumeWithoutDuplicates(MethodInvocationTree tree) { ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null - || !invokedMethod.getSimpleName().contentEquals(WITHOUT_DUPLICATES) - || invokedMethod.getParameters().size() != 1) { - return false; + if (invokedMethod != null + && invokedMethod.getSimpleName().contentEquals(WITHOUT_DUPLICATES) + && invokedMethod.getParameters().size() == 1) { + Element enclosingElement = invokedMethod.getEnclosingElement(); + if (enclosingElement instanceof TypeElement enclosingType) { + return enclosingType.getQualifiedName().contentEquals(COLLECTIONS_PLUME); + } } - - Element enclosingElement = invokedMethod.getEnclosingElement(); - if (!(enclosingElement instanceof TypeElement enclosingType)) { - return false; - } - return enclosingType.getQualifiedName().contentEquals(COLLECTIONS_PLUME); + return false; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 7a768287dce5..49d51b323e9d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -22,7 +22,7 @@ import org.checkerframework.javacutil.TypesUtils; /** - * Base visitor for the Modifiability sub-checkers (Grow, Shrink, Replace). + * Base visitor for the modifiability sub-checkers (Grow, Shrink, Replace). * *

This class contains logic shared across all three sub-checkers: * @@ -66,7 +66,7 @@ public void processClassTree(ClassTree classTree) { super.processClassTree(classTree); if (shouldCheckCustomModifiabilityAnnotation()) { TypeElement typeElement = TreeUtils.elementFromDeclaration(classTree); - if (typeElement != null && isSourceDefinedModifiabilityType(typeElement)) { + if (typeElement != null && isCollectionFromSourceCode(typeElement)) { checker.reportWarning( classTree, "modifiability.annotation.unverified", typeElement.getQualifiedName()); } @@ -207,7 +207,7 @@ protected void checkConstructorResult( * @param typeElement the type element to test * @return true if this source-defined type relies on trusted custom modifiability annotations */ - private boolean isSourceDefinedModifiabilityType(TypeElement typeElement) { + private boolean isCollectionFromSourceCode(TypeElement typeElement) { if (!ElementUtils.isElementFromSourceCode(typeElement)) { return false; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 164351ed6bf7..dba407445c93 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -14,9 +14,9 @@ import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrowable; @@ -48,8 +48,8 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link MaybeGrowable} qualifier (top of Grow hierarchy). */ - private final AnnotationMirror MAYBE_GROW; + /** The {@code @}{@link MaybeGrowable} qualifier. */ + private final AnnotationMirror MAYBE_GROWABLE; /** The {@code @}{@link Growable} qualifier. */ private final AnnotationMirror GROWABLE; @@ -58,9 +58,9 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private final AnnotationMirror UNGROWABLE; /** The {@code @}{@link PolyGrowable} qualifier. */ - private final AnnotationMirror POLY_GROW; + private final AnnotationMirror POLY_GROWABLE; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ + /** The {@code @}{@link IteratorPolyShrinkable} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -71,7 +71,7 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @SuppressWarnings("this-escape") public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - // Cache type erasures. + Types types = getProcessingEnv().getTypeUtils(); this.mapEntryErasure = types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); @@ -80,12 +80,12 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.listIteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_GROW = AnnotationBuilder.fromClass(getElementUtils(), MaybeGrowable.class); + this.MAYBE_GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeGrowable.class); this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); - this.POLY_GROW = AnnotationBuilder.fromClass(getElementUtils(), PolyGrowable.class); + this.POLY_GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyGrowable.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrinkable.class); postInit(); } @@ -99,8 +99,8 @@ protected Set> createSupportedTypeQualifiers() { Ungrowable.class, BottomGrowable.class, PolyGrowable.class, - MaybeIteratorPolyMod.class, - IteratorPolyMod.class)); + MaybeIteratorPolyShrinkable.class, + IteratorPolyShrinkable.class)); } /** @@ -109,9 +109,7 @@ protected Set> createSupportedTypeQualifiers() { * *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that cannot grow structurally, such as {@code Map.Entry} and non-{@code ListIterator} {@code - * Iterator}, their grow component canonicalizes to {@code @MaybeGrowable}. Explicit grow - * qualifiers are left to normal canonicalization so users can still write and preserve an - * explicit capability tuple such as {@code @Growable @Shrinkable @Replaceable Iterator}. + * Iterator}, their grow component canonicalizes to {@code @MaybeGrowable}. * *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrowable}, but * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its @@ -120,15 +118,17 @@ protected Set> createSupportedTypeQualifiers() { @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : GROWABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotGrow(tm) ? MAYBE_GROW : UNGROWABLE; + if (tm != null) { + if (areSameByClass(annotation, Modifiable.class)) { + return typeCannotGrow(tm) ? MAYBE_GROWABLE : GROWABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return typeCannotGrow(tm) ? MAYBE_GROWABLE : UNGROWABLE; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return isMapEntry(tm) ? MAYBE_GROWABLE : POLY_GROWABLE; + } } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_GROW; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && isMapEntry(tm) ? MAYBE_GROW : POLY_GROW; + return MAYBE_GROWABLE; } return super.canonicalAnnotation(annotation); } @@ -153,21 +153,19 @@ protected ParameterizedExecutableType methodFromUse( /** * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. * - *

{@code withoutDuplicates} has a conditional aliasing contract: it may return its argument - * when the argument is already a List with no duplicates, but otherwise it returns a new - * ArrayList. A stub annotation like + *

{@code withoutDuplicates} returns its argument or a new modifiable ArrayList. The signature * *

{@code
    * static  @PolyGrowable List withoutDuplicates(@PolyGrowable Collection values)
    * }
* - * would be unsound, because an {@code @Ungrowable} input could receive the fresh ArrayList, whose - * static type should be {@code @Growable}. It would also be too imprecise to always use + * would be unsound, because an {@code @Ungrowable} input could yield a growable ArrayList, whose + * static type should be {@code @Growable}. It would be sound but imprecise to always use * {@code @MaybeGrowable}, because passing a {@code @Growable} collection guarantees that both * possible results are growable. Therefore, model the method here as preserving {@code @Growable} * inputs and otherwise returning {@code @MaybeGrowable}. * - * @param tree the invocation of {@code withoutDuplicates} + * @param tree an invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method */ private void refineCollectionsPlumeWithoutDuplicatesReturnType( @@ -182,7 +180,7 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. + * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrinkable}. * * @param tree the listIterator method invocation * @param methodType the annotated executable type of the invoked method @@ -192,7 +190,7 @@ private void refineListIteratorReturnType( AnnotatedTypeMirror returnType = methodType.getReturnType(); if (returnType.hasPrimaryAnnotation(UNGROWABLE) || returnType.hasPrimaryAnnotation(GROWABLE) - || returnType.hasPrimaryAnnotation(POLY_GROW)) { + || returnType.hasPrimaryAnnotation(POLY_GROWABLE)) { return; } @@ -211,10 +209,10 @@ private void refineListIteratorReturnType( return; } - if (hasIteratorPolyMod(receiverType)) { + if (hasIteratorPolyShrinkable(receiverType)) { returnType.replaceAnnotation(GROWABLE); } else { - returnType.replaceAnnotation(MAYBE_GROW); + returnType.replaceAnnotation(MAYBE_GROWABLE); } } @@ -241,17 +239,17 @@ private boolean isListIteratorMethod( } /** - * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation */ - private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyShrinkable(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrinkable.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index c4095808193b..ecb8c973b814 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -29,6 +29,6 @@ @StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"growable", "modifiability"}) public class GrowChecker extends ModifiabilityBaseChecker { - /** Creates a Grow checker. */ + /** Creates a GrowChecker. */ public GrowChecker() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub index dadc1d1762fe..c0a3f3ccccb0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub @@ -1,5 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; package com.github.javaparser.ast.stmt; @@ -10,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyMod NodeList getImports() {} + public @Modifiable @IteratorPolyShrinkable NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 405874e70cad..7a8468964911 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -13,8 +13,8 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; import org.checkerframework.checker.modifiability.qual.Modifiable; @@ -57,8 +57,8 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link MaybeReplaceable} qualifier (top of Replace hierarchy). */ - private final AnnotationMirror MAYBE_REPLACE; + /** The {@code @}{@link MaybeReplaceable} qualifier. */ + private final AnnotationMirror MAYBE_REPLACEABLE; /** The {@code @}{@link Replaceable} qualifier. */ private final AnnotationMirror REPLACEABLE; @@ -67,9 +67,9 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private final AnnotationMirror UNREPLACEABLE; /** The {@code @}{@link PolyReplaceable} qualifier. */ - private final AnnotationMirror POLY_REPLACE; + private final AnnotationMirror POLY_REPLACEABLE; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ + /** The {@code @}{@link IteratorPolyShrinkable} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -80,7 +80,7 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @SuppressWarnings("this-escape") public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - // Cache type erasures. + Types types = getProcessingEnv().getTypeUtils(); this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); this.collectionErasure = @@ -94,12 +94,12 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), MaybeReplaceable.class); + this.MAYBE_REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeReplaceable.class); this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); - this.POLY_REPLACE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplaceable.class); + this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplaceable.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrinkable.class); postInit(); } @@ -113,8 +113,8 @@ protected Set> createSupportedTypeQualifiers() { Unreplaceable.class, BottomReplaceable.class, PolyReplaceable.class, - MaybeIteratorPolyMod.class, - IteratorPolyMod.class)); + MaybeIteratorPolyShrinkable.class, + IteratorPolyShrinkable.class)); } /** @@ -124,9 +124,7 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that cannot replace structurally, such as exact {@code Collection}, {@code Set}, non-{@code * LinkedList} {@code Queue}, and non-{@code ListIterator} {@code Iterator}, their replace - * component canonicalizes to {@code @MaybeReplaceable}. Explicit replace qualifiers are left to - * normal canonicalization so users can still write and preserve an explicit capability tuple such - * as {@code @Growable @Shrinkable @Replaceable Set}. + * component canonicalizes to {@code @MaybeReplaceable}. * *

{@code @PolyModifiable} remains {@code @PolyReplaceable}: unlike grow and shrink for {@code * Map.Entry}, replacement through {@code Entry.setValue} is a meaningful capability to carry from @@ -135,15 +133,17 @@ protected Set> createSupportedTypeQualifiers() { @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : REPLACEABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACE : UNREPLACEABLE; + if (tm != null) { + if (areSameByClass(annotation, Modifiable.class)) { + return typeCannotReplace(tm) ? MAYBE_REPLACEABLE : REPLACEABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return typeCannotReplace(tm) ? MAYBE_REPLACEABLE : UNREPLACEABLE; + } } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_REPLACE; + return MAYBE_REPLACEABLE; } else if (areSameByClass(annotation, PolyModifiable.class)) { - return POLY_REPLACE; + return POLY_REPLACEABLE; } return super.canonicalAnnotation(annotation); } @@ -168,21 +168,19 @@ protected ParameterizedExecutableType methodFromUse( /** * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. * - *

{@code withoutDuplicates} has a conditional aliasing contract: it may return its argument - * when the argument is already a List with no duplicates, but otherwise it returns a new - * ArrayList. A stub annotation like + *

{@code withoutDuplicates} returns its argument or a new modifiable ArrayList. The signature * *

{@code
    * static  @PolyReplaceable List withoutDuplicates(@PolyReplaceable Collection values)
    * }
* - * would be unsound, because an {@code @Unreplaceable} input could receive the fresh ArrayList, - * whose static type should be {@code @Replaceable}. It would also be too imprecise to always use + * would be unsound, because an {@code @Unreplaceable} input could yield a replaceable ArrayList, + * whose static type should be {@code @Replaceable}. It would be sound but imprecise to always use * {@code @MaybeReplaceable}, because passing a {@code @Replaceable} collection guarantees that * both possible results are replaceable. Therefore, model the method here as preserving * {@code @Replaceable} inputs and otherwise returning {@code @MaybeReplaceable}. * - * @param tree the invocation of {@code withoutDuplicates} + * @param tree an invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method */ private void refineCollectionsPlumeWithoutDuplicatesReturnType( @@ -197,7 +195,7 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. + * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrinkable}. * * @param tree the listIterator method invocation * @param methodType the annotated executable type of the invoked method @@ -207,7 +205,7 @@ private void refineListIteratorReturnType( AnnotatedTypeMirror returnType = methodType.getReturnType(); if (returnType.hasPrimaryAnnotation(UNREPLACEABLE) || returnType.hasPrimaryAnnotation(REPLACEABLE) - || returnType.hasPrimaryAnnotation(POLY_REPLACE)) { + || returnType.hasPrimaryAnnotation(POLY_REPLACEABLE)) { return; } @@ -226,10 +224,10 @@ private void refineListIteratorReturnType( return; } - if (hasIteratorPolyMod(receiverType)) { + if (hasIteratorPolyShrinkable(receiverType)) { returnType.replaceAnnotation(REPLACEABLE); } else { - returnType.replaceAnnotation(MAYBE_REPLACE); + returnType.replaceAnnotation(MAYBE_REPLACEABLE); } } @@ -256,17 +254,17 @@ private boolean isListIteratorMethod( } /** - * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation */ - private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyShrinkable(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrinkable.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub index dadc1d1762fe..c0a3f3ccccb0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub @@ -1,5 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; package com.github.javaparser.ast.stmt; @@ -10,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyMod NodeList getImports() {} + public @Modifiable @IteratorPolyShrinkable NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index dec77a659a6e..c9fbfa0e50bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -13,8 +13,8 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; @@ -46,8 +46,8 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { // ── Hierarchy qualifiers ────────── - /** The {@code @}{@link MaybeShrinkable} qualifier (top of Shrink hierarchy). */ - private final AnnotationMirror MAYBE_SHRINK; + /** The {@code @}{@link MaybeShrinkable} qualifier. */ + private final AnnotationMirror MAYBE_SHRINKABLE; /** The {@code @}{@link Shrinkable} qualifier. */ private final AnnotationMirror SHRINKABLE; @@ -56,9 +56,9 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private final AnnotationMirror UNSHRINKABLE; /** The {@code @}{@link PolyShrinkable} qualifier. */ - private final AnnotationMirror POLY_SHRINK; + private final AnnotationMirror POLY_SHRINKABLE; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ + /** The {@code @}{@link IteratorPolyShrinkable} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -69,7 +69,7 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @SuppressWarnings("this-escape") public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - // Cache type erasures. + Types types = getProcessingEnv().getTypeUtils(); this.mapEntryErasure = types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); @@ -77,12 +77,12 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrinkable.class); + this.MAYBE_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrinkable.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); - this.POLY_SHRINK = AnnotationBuilder.fromClass(getElementUtils(), PolyShrinkable.class); + this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyShrinkable.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrinkable.class); postInit(); } @@ -96,8 +96,8 @@ protected Set> createSupportedTypeQualifiers() { Unshrinkable.class, BottomShrinkable.class, PolyShrinkable.class, - MaybeIteratorPolyMod.class, - IteratorPolyMod.class)); + MaybeIteratorPolyShrinkable.class, + IteratorPolyShrinkable.class)); } /** @@ -106,9 +106,7 @@ protected Set> createSupportedTypeQualifiers() { * *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on {@code * Map.Entry}, which cannot shrink, their shrink component canonicalizes to - * {@code @MaybeShrinkable}. Explicit shrink qualifiers are left to normal canonicalization so - * users can still write and preserve an explicit capability tuple such as - * {@code @Growable @Shrinkable @Replaceable Entry}. + * {@code @MaybeShrinkable}. * *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrinkable}, but * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its @@ -117,15 +115,17 @@ protected Set> createSupportedTypeQualifiers() { @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : SHRINKABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINK : UNSHRINKABLE; + if (tm != null) { + if (areSameByClass(annotation, Modifiable.class)) { + return typeCannotShrink(tm) ? MAYBE_SHRINKABLE : SHRINKABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return typeCannotShrink(tm) ? MAYBE_SHRINKABLE : UNSHRINKABLE; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return isMapEntry(tm) ? MAYBE_SHRINKABLE : POLY_SHRINKABLE; + } } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_SHRINK; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && isMapEntry(tm) ? MAYBE_SHRINK : POLY_SHRINK; + return MAYBE_SHRINKABLE; } return super.canonicalAnnotation(annotation); } @@ -150,21 +150,19 @@ protected ParameterizedExecutableType methodFromUse( /** * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. * - *

{@code withoutDuplicates} has a conditional aliasing contract: it may return its argument - * when the argument is already a List with no duplicates, but otherwise it returns a new - * ArrayList. A stub annotation like + *

{@code withoutDuplicates} returns its argument or a new modifiable ArrayList. The signature * *

{@code
    * static  @PolyShrinkable List withoutDuplicates(@PolyShrinkable Collection values)
    * }
* - * would be unsound, because an {@code @Unshrinkable} input could receive the fresh ArrayList, - * whose static type should be {@code @Shrinkable}. It would also be too imprecise to always use + * would be unsound, because an {@code @Unshrinkable} input could yield a shrinkable ArrayList, + * whose static type should be {@code @Shrinkable}. It would be sound but imprecise to always use * {@code @MaybeShrinkable}, because passing a {@code @Shrinkable} collection guarantees that both * possible results are shrinkable. Therefore, model the method here as preserving * {@code @Shrinkable} inputs and otherwise returning {@code @MaybeShrinkable}. * - * @param tree the invocation of {@code withoutDuplicates} + * @param tree an invocation of {@code withoutDuplicates} * @param methodType the annotated executable type of the invoked method */ private void refineCollectionsPlumeWithoutDuplicatesReturnType( @@ -179,10 +177,10 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code iterator()} return type based on {@code @IteratorPolyMod}. + * Refines {@code iterator()} return type based on {@code @IteratorPolyShrinkable}. * *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked - * method receiver type has {@code @IteratorPolyMod}, then the result is {@code @Shrinkable + * method receiver type has {@code @IteratorPolyShrinkable}, then the result is {@code @Shrinkable * Iterator}. Otherwise, shrinkability precision is dropped to {@code @MaybeShrinkable}. * * @param tree the iterator method invocation @@ -195,7 +193,7 @@ private void refineIteratorReturnType( // ArrayList). if (returnType.hasPrimaryAnnotation(UNSHRINKABLE) || returnType.hasPrimaryAnnotation(SHRINKABLE) - || returnType.hasPrimaryAnnotation(POLY_SHRINK)) { + || returnType.hasPrimaryAnnotation(POLY_SHRINKABLE)) { return; } @@ -214,11 +212,11 @@ private void refineIteratorReturnType( return; } - // receiver type is @Shrinkable. check for @IteratorPolyMod - if (hasIteratorPolyMod(receiverType)) { + // receiver type is @Shrinkable. check for @IteratorPolyShrinkable + if (hasIteratorPolyShrinkable(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { - returnType.replaceAnnotation(MAYBE_SHRINK); + returnType.replaceAnnotation(MAYBE_SHRINKABLE); } } @@ -248,17 +246,17 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT } /** - * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation */ - private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyShrinkable(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrinkable.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub index dadc1d1762fe..c0a3f3ccccb0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub @@ -1,5 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; package com.github.javaparser.ast.stmt; @@ -10,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyMod NodeList getImports() {} + public @Modifiable @IteratorPolyShrinkable NodeList getImports() {} } diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties index 194399bf4a2b..0b07066578da 100644 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties @@ -1,3 +1,3 @@ usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. unmodparam.location=@UnmodifiableParam may only be written on method/constructor parameters or explicit receiver parameters. -modifiability.annotation.unverified=Modifiability annotations/defaults on source-defined collection/map/iterator type %s are trusted but not verified by the checker; manually verify them and suppress this warning if correct. +modifiability.annotation.unverified=Modifiability annotations on collection/map/iterator type %s cannot be verified by the checker. diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 90e4f6352b9d..10d5043af59e 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -1,7 +1,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -112,7 +112,7 @@ void testUnmodifiableParamMutation(@UnmodifiableParam List unmodParam, S } void testReplaceAll( - @IteratorPolyMod @Modifiable List mod, @Unmodifiable List unmod) { + @IteratorPolyShrinkable @Modifiable List mod, @Unmodifiable List unmod) { mod.replaceAll(s -> s + "!"); // :: error: [method.invocation] diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index eab62642ce4e..596a02c67d61 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -30,7 +30,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -43,7 +43,7 @@ enum TestEnum { B } - @IteratorPolyMod List list; + @IteratorPolyShrinkable List list; void iteratorChecker() { // this should not be allowed because CopyOnWriteArrayList's iterator does not preserve remove @@ -73,7 +73,7 @@ void copyOnWriteArrayListIterator() { List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), - // then if the receiver is @Shrinkable and has @IteratorPolyMod, then the result is + // then if the receiver is @Shrinkable and has @IteratorPolyShrinkable, then the result is // @Shrinkable. // otherwise, the result is unknown. // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? @@ -133,7 +133,7 @@ void KeySetIterator() { @Shrinkable Iterator iter2 = keys2.iterator(); } - void setIteratorPolyMod() { + void setIteratorPolyShrinkable() { Set hashSet = new HashSet<>(); @Shrinkable Iterator hashSetIterator = hashSet.iterator(); hashSetIterator.remove(); @@ -143,7 +143,7 @@ void setIteratorPolyMod() { treeSetIterator.remove(); } - void dequeIteratorPolyMod() { + void dequeIteratorPolyShrinkable() { Deque arrayDeque = new ArrayDeque<>(); @Shrinkable Iterator arrayDequeIterator = arrayDeque.iterator(); arrayDequeIterator.remove(); @@ -153,7 +153,7 @@ void dequeIteratorPolyMod() { linkedListIterator.remove(); } - void queueIteratorPolyMod() { + void queueIteratorPolyShrinkable() { Queue priorityQueue = new PriorityQueue<>(); @Shrinkable Iterator priorityQueueIterator = priorityQueue.iterator(); priorityQueueIterator.remove(); @@ -171,7 +171,7 @@ void queueIteratorPolyMod() { linkedTransferQueueIterator.remove(); } - void factoryIteratorPolyMod() { + void factoryIteratorPolyShrinkable() { Set enumSet = EnumSet.of(TestEnum.A, TestEnum.B); @Shrinkable Iterator enumSetIterator = enumSet.iterator(); enumSetIterator.remove(); @@ -245,7 +245,7 @@ void unmodifiableMapWrapperViewIteratorsDoNotPreserveRemove() { @Unshrinkable Iterator> entryIterator = map.entrySet().iterator(); } - void iteratorDependentCollectionMethodsRequireIteratorPolyMod( + void iteratorDependentCollectionMethodsRequireIteratorPolyShrinkable( @Modifiable IteratorRemoveUnsupportedCollection collection, @Modifiable IteratorRemoveUnsupportedSet set) { // :: error: [method.invocation] @@ -263,7 +263,7 @@ void iteratorDependentCollectionMethodsRequireIteratorPolyMod( set.removeAll(List.of("a")); } - void iteratorDependentListMethodsRequireIteratorPolyMod( + void iteratorDependentListMethodsRequireIteratorPolyShrinkable( @Modifiable IteratorMutationUnsupportedList list) { // :: error: [method.invocation] list.set(0, "b"); diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 808014e8ef4b..fac922b0eb60 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -4,10 +4,22 @@ In Java, an \emph{optional} method is one that a class may or may not support. (Optional methods are unrelated to the \ type.) -If the class does not support the method, then calling the -method throws \UnsupportedOperationException. Thus, whenever a client -calls an optional method, there is a possibility of an exception. The -Modifiability Checker eliminates that possibility. +An example is +\sunjavadoc{java.base/java/util/Collection.html\#add(E)}{add}. Some +subclasses of \ support \, and others (such as the +result of calling +\sunjavadoc{java.base/java/util/Collections.html\#unmodifiableCollection(java.util.Collection)}{Collections.unmodifiableCollection}) +do not support \. + +Calling an unsupported method throws \UnsupportedOperationException. Thus, +whenever a client calls an optional method, there is a possibility of an +exception. The Modifiability Checker eliminates that possibility. + +To run the Modifiability Checker: + +\begin{Verbatim} +javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java +\end{Verbatim} If the Modifiability Checker issues no warning, then your program is guaranteed not to throw \UnsupportedOperationException\ due to invoking an @@ -18,6 +30,13 @@ (For brevity, this chapter generally uses just ``collection'' to mean ``collection, map, or iterator''.) +It is possible to write a checker that warns about other unsupported +methods (beyond those in the Java Collections Framework), but the +Modifiability Checker does not do so. + + +\sectionAndLabel{Unmodifiable collections}{modifiability-unmodifiable-collections} + An \emph{unmodifiable collection} is one that throws \UnsupportedOperationException\ if a mutating method is called. The JDK provides many such collections, for example, the results of: @@ -42,8 +61,8 @@ \end{itemize} A \emph{modifiable collection} is one on which mutating methods can be called -without throwing \UnsupportedOperationException. Many other methods, such -as \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be +without throwing \UnsupportedOperationException. Non-optional methods, such +as \code{size}, \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be called on any collection, whether modifiable or unmodifiable. Some collections may be modified in some ways but not in others. @@ -51,7 +70,11 @@ \textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements - (\, \, \, \, \, \ldots). + (\, \, \, \ldots). + \item \textbf{GrowSequenced}: The collection can grow by adding new + elements at the beginning or end (\, \). + Because GrowSequenced checking is exactly like Grow checking for a + different set of methods, this chapter often omits it for brevity. \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing @@ -62,18 +85,13 @@ \noindent Some collections can also be modified through their iterator; see Section~\ref{modifiability-iterator-behavior}. -To run the Modifiability Checker: - -\begin{Verbatim} -javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java -\end{Verbatim} \sectionAndLabel{Modifiability annotations}{modifiability-annotations} -The Modifiability Checker uses \textbf{three independent 4-element -hierarchies}, one per capability. Every annotated type carries exactly -one qualifier from each hierarchy. The checker also uses a fourth, +The Modifiability Checker uses \textbf{multiple independent 4-element +hierarchies}, one per capability. Every collection type carries exactly +one qualifier from each hierarchy. The checker also uses an additional 2-element hierarchy to express whether \ can be called on a collection's iterator. @@ -85,26 +103,28 @@ \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @MaybeGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyMod + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyShrinkable / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | - @BottomGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyMod + @BottomGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyShrinkable \end{verbatim} \end{minipage} \end{smaller} - \caption{The four independent Modifiability qualifier hierarchies. + \caption{Independent modifiability qualifier hierarchies. + (The GrowSequenced hierarchy is omitted for brevity.) Each annotated type carries one qualifier from each hierarchy.} \label{fig-modifiability-lattice} \end{figure} Figure~\ref{fig-modifiability-lattice} shows the hierarchies. +Each hierarchy is type-checked individually. An assignment \code{T x = y} is valid only if the compile-time type of \code{y} is a subtype of (or equal to) \code{T} in every hierarchy simultaneously. For -example, \code{@Growable @MaybeShrinkable @MaybeReplaceable List} is a -subtype of \code{@MaybeGrowable @MaybeShrinkable @MaybeReplaceable List} +example, \code{\emph{@Growable} @MaybeShrinkable @MaybeReplaceable List} is a +subtype of \code{\emph{@MaybeGrowable} @MaybeShrinkable @MaybeReplaceable List} (because \<@Growable> $\le$ \<@MaybeGrowable>), but it is \emph{not} a -subtype of \code{@Ungrowable @MaybeShrinkable @MaybeReplaceable List} +subtype of \code{\emph{@Ungrowable} @MaybeShrinkable @MaybeReplaceable List} (because \<@Growable> and \<@Ungrowable> are incomparable siblings in the Grow hierarchy). @@ -136,11 +156,22 @@ shrink, or replace operation. \item[\refqualclass{checker/modifiability/qual}{UnmodifiableParam}] -A syntactic sugar for \refqualclass{checker/modifiability/qual}{MaybeModifiable}. -It may only be written within a method or constructor parameter type, or within -an explicit receiver parameter type, when the method is not designed to change -the parameter. It may appear nested within such a parameter type, such as in -\code{List<@UnmodifiableParam List>}. +Syntactic sugar for \refqualclass{checker/modifiability/qual}{MaybeModifiable}. +It may only be written within a formal parameter type to indicate that the +method does not modify the parameter. This annotation (and +\refqualclass{checker/modifiability/qual}{MaybeModifiable}) enable the +programmer to distinguish between a data structure's capability to perform +a modification and the programmer's intent about whether a modification +should occur. +\refqualclass{checker/modifiability/qual}{UnmodifiableParam} +may appear nested within a parameter type, such as in +\code{List<@UnmodifiableParam List\relax>}. + +\refqualclass{checker/modifiability/qual}{UnmodifiableParam} represents +\emph{reference unmodifiability}: no modification may happen through the +reference, but an alias may modify the value. By contrast, +\refqualclass{checker/modifiability/qual}{Unmodifiable} is object +unmodifiability: no alias may modify the value. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable} @@ -158,106 +189,125 @@ void foo(@UnmodifiableParam List list) { ... } // foo does not modify list \end{Verbatim} -\subsectionAndLabel{Hierarchy qualifiers}{modifiability-hierarchy-qualifiers} +\subsectionAndLabel{Qualifiers}{modifiability-qualifiers} + +\subsubsectionAndLabel{The Grow hierarchy}{modifiability-grow-hierarchy} -Qualifiers in the Grow hierarchy: \begin{description} \item[\refqualclass{checker/modifiability/qual}{MaybeGrowable}] The top qualifier in the Grow hierarchy. The checker cannot determine whether this collection supports grow -operations. This is the default qualifier for unannotated types in the +operations. +Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this +collection \emph{may} throw \UnsupportedOperationException. +This is the default qualifier for unannotated types in the Grow hierarchy. \item[\refqualclass{checker/modifiability/qual}{Growable}] Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this -collection will not result in throwing \UnsupportedOperationException. +collection \emph{will not} throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Ungrowable}] Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this -collection will throw \UnsupportedOperationException. +collection \emph{will} throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{BottomGrowable}] The bottom qualifier in the Grow hierarchy. Programmers should rarely write it. \end{description} -Qualifiers in the Shrink hierarchy: +\subsubsectionAndLabel{The GrowSequenced hierarchy}{modifiability-grow-hierarchy} + +\begin{description} +\item[\refqualclass{checker/modifiability/qual}{MaybeGrowableSequenced}] +The top qualifier in the GrowSequenced hierarchy. +The checker cannot determine whether this collection supports grow +operations. +Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this +collection \emph{may} throw \UnsupportedOperationException. +This is the default qualifier for unannotated types in the +GrowSequenced hierarchy. + +\item[\refqualclass{checker/modifiability/qual}{GrowableSequenced}] +Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this +collection \emph{will not} throw \UnsupportedOperationException. + +\item[\refqualclass{checker/modifiability/qual}{UngrowableSequenced}] +Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this +collection \emph{will} throw \UnsupportedOperationException. + +\item[\refqualclass{checker/modifiability/qual}{BottomGrowableSequenced}] +The bottom qualifier in the GrowSequenced hierarchy. Programmers should rarely write it. +\end{description} + +\subsubsectionAndLabel{The Shrink hierarchy}{modifiability-shrink-hierarchy} + \begin{description} \item[\refqualclass{checker/modifiability/qual}{MaybeShrinkable}] The top qualifier in the Shrink hierarchy. The checker cannot determine whether this collection supports shrink -operations. This is the default qualifier for unannotated types in the +operations. +Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on +this collection \emph{may} throw \UnsupportedOperationException. +This is the default qualifier for unannotated types in the Shrink hierarchy. \item[\refqualclass{checker/modifiability/qual}{Shrinkable}] Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on -this collection will not result in throwing -\UnsupportedOperationException. +this collection \emph{will not} throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Unshrinkable}] Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on -this collection will throw \UnsupportedOperationException. +this collection \emph{will} throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{BottomShrinkable}] The bottom qualifier in the Shrink hierarchy. Programmers should rarely write it. \end{description} -Qualifiers in the Replace hierarchy: +\subsubsectionAndLabel{The Replace hierarchy}{modifiability-replace-hierarchy} + \begin{description} \item[\refqualclass{checker/modifiability/qual}{MaybeReplaceable}] The top qualifier in the Replace hierarchy. The checker cannot determine whether this collection supports replace -operations. This is the default qualifier for unannotated types in the +operations. +Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ +on this collection \emph{may} throw \UnsupportedOperationException. +This is the default qualifier for unannotated types in the Replace hierarchy. \item[\refqualclass{checker/modifiability/qual}{Replaceable}] Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ -on this collection will not result in throwing -\UnsupportedOperationException. +on this collection \emph{will not} throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Unreplaceable}] Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ -on this collection will throw \UnsupportedOperationException. +on this collection \emph{will} throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{BottomReplaceable}] The bottom qualifier in the Replace hierarchy. Programmers should rarely write it. \end{description} -Qualifiers in the Iterator hierarchy: +\subsubsectionAndLabel{The Iterator hierarchy}{modifiability-iterator-hierarchy} + \begin{description} -\item[\refqualclass{checker/modifiability/qual}{MaybeIteratorPolyMod}] +\item[\refqualclass{checker/modifiability/qual}{MaybeIteratorPolyShrinkable}] The top qualifier. The checker cannot determine whether this collection's \code{iterator()} is \<@PolyShrinkable>. This is the default qualifier for unannotated types. -\item[\refqualclass{checker/modifiability/qual}{IteratorPolyMod}] +\item[\refqualclass{checker/modifiability/qual}{IteratorPolyShrinkable}] This collection's \code{iterator()} is \<@PolyShrinkable>. That is, if collection \ is \<@Shrinkable>, then \ is also \<@Shrinkable>. \end{description} -\subsectionAndLabel{Combining capabilities}{modifiability-combining} - -To express multiple capabilities, write one annotation per hierarchy: - -\begin{Verbatim} - @Growable @Shrinkable List // can grow and shrink, maybe replace - @Growable @Replaceable List // can grow and replace, maybe shrink - @Growable @Shrinkable @Replaceable List // all three capabilities -\end{Verbatim} - -Because the three hierarchies are independent, subtyping checks each -hierarchy individually. A \code{@Growable @Shrinkable List} is a -subtype of \code{@Growable List} (Shrink defaults to \<@MaybeShrinkable> on the -right, Replace -defaults to \<@MaybeReplaceable> on both sides), but it is \emph{not} a -subtype of \code{@Growable @Shrinkable @Replaceable List} (due to the Replace -qualifiers). \subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} -A polymorphic qualifier specifies that a method's return type has the +A polymorphic qualifier (Section~\ref{method-qualifier-polymorphism}) +specifies that a method's return type has the same qualifier (in some hierarchy) as a formal parameter type (possibly the receiver). @@ -283,29 +333,11 @@ \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable}. Preserves all three capabilities simultaneously. -Use on methods such as \code{Collections.synchronizedList} that are pure -wrappers and do not change modifiability. +Use on methods such as \code{Collections.synchronizedList} thatx +do not change modifiability. \end{description} -\noindent -Because the three poly qualifiers are independent, you can mix -polymorphic and concrete qualifiers across hierarchies. For example: - -\begin{Verbatim} - @Ungrowable @PolyShrinkable Set keySet(@PolyShrinkable Map map) -\end{Verbatim} - -If the map is \code{@Growable @Shrinkable @Replaceable}, then -\code{keySet} returns -\code{@Ungrowable @Shrinkable @MaybeReplaceable Set}. - -If the map is \code{@MaybeGrowable @MaybeShrinkable @MaybeReplaceable}, then -\code{keySet} returns \code{@Ungrowable @MaybeShrinkable @MaybeReplaceable Set}. - -If the map is \code{@Ungrowable @Unshrinkable @Unreplaceable}, then -\code{keySet} returns \code{@Ungrowable @Unshrinkable @MaybeReplaceable Set}. - \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} @@ -325,40 +357,58 @@ \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} -Some Java types lack methods for certain kinds of mutation. -For example, \sunjavadoc{java.base/java/util/Queue.html}{\code{Queue}} has -\ and \ methods but no replacement methods. However, the type -\<@Replaceable Queue> still makes sense; for example, it is the type of -\ in \. +%% TODO add explicit examples TODO, also in the Javadoc TODO. + +Ordinarily, the declaration +\<@Modifiable MyClass x;> is equivalent to +\<@Growable @Shrinkable @Replaceable MyClass x;>, +and +\<@Unodifiable MyClass x;> is equivalent to +\<@Ungrowable @Unshrinkable @Unreplaceable MyClass x;>. However, there are +exceptions: + +%% TODO add more examples, exhaustively if possible. +\begin{tabular}{rlcrrrl} +% \<@Modifiable X> & = & \<@Growable> & \<@Shrinkable> & \<@Replaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@MaybeGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@MaybeGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\end{tabular} When a programmer writes \<@Modifiable> or \<@Unmodifiable> on a type that -has no method for a given mutation, the Modifiability Checker uses the top -qualifier. For example, both \<@Modifiable Queue> and \<@Unmodifiable -Queue> have \<@MaybeReplaceable> as their replaceability qualifier. (In the -above assignment, \ was flow-sensitively refined to have type -\<@Replaceable>.) Other examples are that \ has no replace methods -and \ has no grow or replace methods. +has no methods for a given mutation, the Modifiability Checker uses the top +qualifier. +For example, \sunjavadoc{java.base/java/util/Queue.html}{\code{Queue}} has +\ and \ methods but no replacement methods. + +Although no object of class \ supports replace operations, +the type +\<@Replaceable Queue> still makes sense, because some objects of type +\ do support replace operations. +For example, \<@Replaceable Queue> is the type of +\ in \. This behavior ensures that code like this type-checks: \begin{Verbatim} @Growable LinkedList list = ...; Queue q = list; -@Growable LinkedList list2 = ...; // still @Growable +@Growable LinkedList list2 = (Queue) q; // still @Growable \end{Verbatim} \sectionAndLabel{Behavior of the \ method}{modifiability-iterator-behavior} -By default, calling \ returns an \<@Unshrinkable -Iterator>; that is, the signature of \ is +By default, the signature of \ is \begin{Verbatim} @Unshrinkable Iterator iterator() \end{Verbatim} -However, some subclasses of \ can return a shrinkable -iterator. For such subclasses of \, the signature of +However, for some subclasses of \, the signature of \ is \begin{Verbatim} @@ -366,13 +416,13 @@ \end{Verbatim} A collection uses the second signature if the collection is -\<@IteratorPolyMod>. The collection uses the first signature if the -collection is \<@MaybeIteratorPolyMod>. +\<@IteratorPolyShrinkable>. The collection uses the first signature if the +collection is \<@MaybeIteratorPolyShrinkable>. Here are examples of its use: \begin{Verbatim} - @IteratorPolyMod @Shrinkable List list = ...; + @IteratorPolyShrinkable @Shrinkable List list = ...; @Shrinkable Iterator it = list.iterator(); it.remove(); // OK @@ -452,4 +502,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable -% LocalWords: MaybeIteratorPolyMod IteratorPolyMod +% LocalWords: MaybeIteratorPolyShrinkable IteratorPolyShrinkable From ae363097aae6bb2bcd7a2b9c841c4d3445185fa4 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 13:20:24 -0700 Subject: [PATCH 186/308] documented refineListIteratorReturnType Co-authored-by: Copilot --- .../grow/GrowAnnotatedTypeFactory.java | 9 +++++++++ .../replace/ReplaceAnnotatedTypeFactory.java | 9 +++++++++ .../shrink/ShrinkAnnotatedTypeFactory.java | 14 ++++++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index f717e5aaaedc..69f89bf2e386 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -182,6 +182,15 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( /** * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrinkable}. * + *

{@code listIterator()} cannot be annotated as {@code @PolyModifiable} because not all + * collections preserve the modifiability of their iterators. (For example, {@code + * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, + * special treatment is needed for Iterator methods. + * + *

If the receiver is {@code @Growable} and {@code @IteratorPolyShrinkable}, then the result is + * {@code @Growable Iterator}. Otherwise, growability precision is dropped to + * {@code @MaybeGrowable}. + * * @param tree the listIterator method invocation * @param methodType the annotated executable type of the invoked method */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 8c8570674596..ea85de654737 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -197,6 +197,15 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( /** * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrinkable}. * + *

{@code listIterator()} cannot be annotated as {@code @PolyModifiable} because not all + * collections preserve the modifiability of their iterators. (For example, {@code + * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, + * special treatment is needed for Iterator methods. + * + *

If the receiver is {@code @Replaceable} and {@code @IteratorPolyShrinkable}, then the result + * is {@code @Replaceable Iterator}. Otherwise, replaceability precision is dropped to + * {@code @MaybeReplaceable}. + * * @param tree the listIterator method invocation * @param methodType the annotated executable type of the invoked method */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 981d99adc7d5..356cad73a707 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -177,11 +177,17 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code iterator()} return type based on {@code @IteratorPolyShrinkable}. + * Refines {@code iterator()} and {@code listIterator()} return types based on + * {@code @IteratorPolyShrinkable}. * - *

If the receiver is {@code @Shrinkable} and either the receiver type use or the invoked - * method receiver type has {@code @IteratorPolyShrinkable}, then the result is {@code @Shrinkable - * Iterator}. Otherwise, shrinkability precision is dropped to {@code @MaybeShrinkable}. + *

{@code iterator()} and {@code listIterator()} cannot be annotated as {@code @PolyModifiable} + * because not all collections preserve the modifiability of their iterators. (For example, {@code + * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, + * special treatment is needed for Iterator methods. + * + *

If the receiver is {@code @Shrinkable} and {@code @IteratorPolyShrinkable}, then the result + * is {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to + * {@code @MaybeShrinkable}. * * @param tree the iterator method invocation * @param methodType the annotated executable type of the invoked method From e8419f2261b31a68896ff84767b9802973dbce54 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 14:18:40 -0700 Subject: [PATCH 187/308] documented isListIteratorMethod and isIteratorMethod. Co-authored-by: Copilot --- .../checker/modifiability/grow/GrowAnnotatedTypeFactory.java | 5 +++-- .../modifiability/replace/ReplaceAnnotatedTypeFactory.java | 5 +++-- .../modifiability/shrink/ShrinkAnnotatedTypeFactory.java | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 69f89bf2e386..fb158c5a2e1a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -238,13 +238,14 @@ private boolean isListIteratorMethod( if (invokedMethod == null) { return false; } + // quick syntax check before expensive erasure checks if (!invokedMethod.getSimpleName().contentEquals("listIterator") || tree.getArguments().size() > 1) { return false; } - + // Check if the return type is an erased ListIterator TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); + return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index ea85de654737..be1e19782f01 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -253,13 +253,14 @@ private boolean isListIteratorMethod( if (invokedMethod == null) { return false; } + // quick syntax check before expensive erasure checks if (!invokedMethod.getSimpleName().contentEquals("listIterator") || tree.getArguments().size() > 1) { return false; } - + // Check if the return type is an erased ListIterator TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); + return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 356cad73a707..a7874f52c76c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -240,13 +240,14 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT if (invokedMethod == null) { return false; } + // quick syntax check before expensive erasure checks String methodName = invokedMethod.getSimpleName().toString(); int argCount = tree.getArguments().size(); if (!((methodName.equals("iterator") && argCount == 0) || (methodName.equals("listIterator") && argCount <= 1))) { return false; } - + // Check if the return type is an erased Iterator TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); } From 55d8d48f0d135b796ac8a5a56ce9c0faee9559d8 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 14:43:38 -0700 Subject: [PATCH 188/308] rename iteratorPolyShrinkable back to IteratorPolyMod Co-authored-by: Copilot --- .../afu/scenelib/util/coll/WrapperMap.java | 4 +-- buildSrc/src/main/groovy/cf-spotless.gradle | 4 +-- ...lyShrinkable.java => IteratorPolyMod.java} | 8 ++--- ...inkable.java => MaybeIteratorPolyMod.java} | 4 +-- .../grow/GrowAnnotatedTypeFactory.java | 26 ++++++++-------- .../modifiability/grow/javaparser.astub | 4 +-- .../replace/ReplaceAnnotatedTypeFactory.java | 28 ++++++++--------- .../modifiability/replace/javaparser.astub | 4 +-- .../shrink/ShrinkAnnotatedTypeFactory.java | 30 +++++++++---------- .../modifiability/shrink/javaparser.astub | 4 +-- .../modifiability/BasicModifiabilityTest.java | 4 +-- .../modifiability/IteratorPrecisionTest.java | 18 +++++------ docs/manual/modifiability-checker.tex | 16 +++++----- 13 files changed, 77 insertions(+), 77 deletions(-) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{IteratorPolyShrinkable.java => IteratorPolyMod.java} (72%) rename checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/{MaybeIteratorPolyShrinkable.java => MaybeIteratorPolyMod.java} (90%) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 70734ddc7e81..dce3e04ee2c1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,7 +3,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; /** @@ -40,7 +40,7 @@ public boolean containsValue(Object value) { @SuppressWarnings("keyfor") // use of delegate object @Override - public @IteratorPolyShrinkable @PolyShrinkable Set> entrySet( + public @IteratorPolyMod @PolyShrinkable Set> entrySet( @PolyShrinkable WrapperMap this) { return back.entrySet(); } diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index a41bb6f9ef8d..ac83e54b0f7b 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -96,7 +96,7 @@ spotless { .addTypeAnnotation("BottomReplaceable") .addTypeAnnotation("BottomShrinkable") .addTypeAnnotation("Growable") - .addTypeAnnotation("IteratorPolyShrinkable") + .addTypeAnnotation("IteratorPolyMod") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrowable") .addTypeAnnotation("PolyModifiable") @@ -106,7 +106,7 @@ spotless { .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") .addTypeAnnotation("MaybeGrowable") - .addTypeAnnotation("MaybeIteratorPolyShrinkable") + .addTypeAnnotation("MaybeIteratorPolyMod") .addTypeAnnotation("MaybeModifiable") .addTypeAnnotation("MaybeReplaceable") .addTypeAnnotation("MaybeShrinkable") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java similarity index 72% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrinkable.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index 0e41344e8a5f..7bddf03970bf 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyShrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -8,10 +8,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * If a collection's type is {@code @IteratorPolyShrinkable}, then its {@code iterator()} method + * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} method * preserves the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has * type {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any - * collection whose type is {@code @MaybeIteratorPolyShrinkable}, its iterator is always + * collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is always * {@code @Unshrinkable}. * * @checker_framework.manual #modifiability-checker Modifiability Checker @@ -19,5 +19,5 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MaybeIteratorPolyShrinkable.class) -public @interface IteratorPolyShrinkable {} +@SubtypeOf(MaybeIteratorPolyMod.class) +public @interface IteratorPolyMod {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyShrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java similarity index 90% rename from checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyShrinkable.java rename to checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java index e48346a6d48d..f113fdc671f6 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyShrinkable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java @@ -13,7 +13,7 @@ * *

This is the default qualifier for unannotated types. * - * @see IteratorPolyShrinkable + * @see IteratorPolyMod * @checker_framework.manual #modifiability-checker Modifiability Checker */ @Documented @@ -21,4 +21,4 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy -public @interface MaybeIteratorPolyShrinkable {} +public @interface MaybeIteratorPolyMod {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index fb158c5a2e1a..129ec92beb98 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -14,9 +14,9 @@ import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrowable; @@ -60,7 +60,7 @@ public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyGrowable} qualifier. */ private final AnnotationMirror POLY_GROWABLE; - /** The {@code @}{@link IteratorPolyShrinkable} qualifier. */ + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -85,7 +85,7 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyGrowable.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrinkable.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); postInit(); } @@ -99,8 +99,8 @@ protected Set> createSupportedTypeQualifiers() { Ungrowable.class, BottomGrowable.class, PolyGrowable.class, - MaybeIteratorPolyShrinkable.class, - IteratorPolyShrinkable.class)); + MaybeIteratorPolyMod.class, + IteratorPolyMod.class)); } /** @@ -180,14 +180,14 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrinkable}. + * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. * *

{@code listIterator()} cannot be annotated as {@code @PolyModifiable} because not all * collections preserve the modifiability of their iterators. (For example, {@code * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, * special treatment is needed for Iterator methods. * - *

If the receiver is {@code @Growable} and {@code @IteratorPolyShrinkable}, then the result is + *

If the receiver is {@code @Growable} and {@code @IteratorPolyMod}, then the result is * {@code @Growable Iterator}. Otherwise, growability precision is dropped to * {@code @MaybeGrowable}. * @@ -218,7 +218,7 @@ private void refineListIteratorReturnType( return; } - if (hasIteratorPolyShrinkable(receiverType)) { + if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(GROWABLE); } else { returnType.replaceAnnotation(MAYBE_GROWABLE); @@ -249,17 +249,17 @@ private boolean isListIteratorMethod( } /** - * Returns true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation */ - private boolean hasIteratorPolyShrinkable(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrinkable.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub index c0a3f3ccccb0..dadc1d1762fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub @@ -1,5 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; @@ -10,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyShrinkable NodeList getImports() {} + public @Modifiable @IteratorPolyMod NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index be1e19782f01..3a148dc6607b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -13,8 +13,8 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; import org.checkerframework.checker.modifiability.qual.Modifiable; @@ -69,7 +69,7 @@ public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyReplaceable} qualifier. */ private final AnnotationMirror POLY_REPLACEABLE; - /** The {@code @}{@link IteratorPolyShrinkable} qualifier. */ + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -99,7 +99,7 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplaceable.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrinkable.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); postInit(); } @@ -113,8 +113,8 @@ protected Set> createSupportedTypeQualifiers() { Unreplaceable.class, BottomReplaceable.class, PolyReplaceable.class, - MaybeIteratorPolyShrinkable.class, - IteratorPolyShrinkable.class)); + MaybeIteratorPolyMod.class, + IteratorPolyMod.class)); } /** @@ -195,15 +195,15 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( } /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyShrinkable}. + * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. * *

{@code listIterator()} cannot be annotated as {@code @PolyModifiable} because not all * collections preserve the modifiability of their iterators. (For example, {@code * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, * special treatment is needed for Iterator methods. * - *

If the receiver is {@code @Replaceable} and {@code @IteratorPolyShrinkable}, then the result - * is {@code @Replaceable Iterator}. Otherwise, replaceability precision is dropped to + *

If the receiver is {@code @Replaceable} and {@code @IteratorPolyMod}, then the result is + * {@code @Replaceable Iterator}. Otherwise, replaceability precision is dropped to * {@code @MaybeReplaceable}. * * @param tree the listIterator method invocation @@ -233,7 +233,7 @@ private void refineListIteratorReturnType( return; } - if (hasIteratorPolyShrinkable(receiverType)) { + if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(REPLACEABLE); } else { returnType.replaceAnnotation(MAYBE_REPLACEABLE); @@ -264,17 +264,17 @@ private boolean isListIteratorMethod( } /** - * Returns true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation */ - private boolean hasIteratorPolyShrinkable(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrinkable.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub index c0a3f3ccccb0..dadc1d1762fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub @@ -1,5 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; @@ -10,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyShrinkable NodeList getImports() {} + public @Modifiable @IteratorPolyMod NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index a7874f52c76c..38af650b3ac6 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -13,8 +13,8 @@ import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; @@ -58,7 +58,7 @@ public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link PolyShrinkable} qualifier. */ private final AnnotationMirror POLY_SHRINKABLE; - /** The {@code @}{@link IteratorPolyShrinkable} qualifier. */ + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** @@ -82,7 +82,7 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyShrinkable.class); this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyShrinkable.class); + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); postInit(); } @@ -96,8 +96,8 @@ protected Set> createSupportedTypeQualifiers() { Unshrinkable.class, BottomShrinkable.class, PolyShrinkable.class, - MaybeIteratorPolyShrinkable.class, - IteratorPolyShrinkable.class)); + MaybeIteratorPolyMod.class, + IteratorPolyMod.class)); } /** @@ -178,15 +178,15 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( /** * Refines {@code iterator()} and {@code listIterator()} return types based on - * {@code @IteratorPolyShrinkable}. + * {@code @IteratorPolyMod}. * *

{@code iterator()} and {@code listIterator()} cannot be annotated as {@code @PolyModifiable} * because not all collections preserve the modifiability of their iterators. (For example, {@code * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, * special treatment is needed for Iterator methods. * - *

If the receiver is {@code @Shrinkable} and {@code @IteratorPolyShrinkable}, then the result - * is {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to + *

If the receiver is {@code @Shrinkable} and {@code @IteratorPolyMod}, then the result is + * {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to * {@code @MaybeShrinkable}. * * @param tree the iterator method invocation @@ -218,8 +218,8 @@ private void refineIteratorReturnType( return; } - // receiver type is @Shrinkable. check for @IteratorPolyShrinkable - if (hasIteratorPolyShrinkable(receiverType)) { + // receiver type is @Shrinkable. check for @IteratorPolyMod + if (hasIteratorPolyMod(receiverType)) { returnType.replaceAnnotation(SHRINKABLE); } else { returnType.replaceAnnotation(MAYBE_SHRINKABLE); @@ -253,17 +253,17 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT } /** - * Returns true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation. + * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. * * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyShrinkable} marker annotation + * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation */ - private boolean hasIteratorPolyShrinkable(AnnotatedTypeMirror type) { + private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { return true; } return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyShrinkable.class); + type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub index c0a3f3ccccb0..dadc1d1762fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub @@ -1,5 +1,5 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; @@ -10,5 +10,5 @@ class BlockStmt { package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyShrinkable NodeList getImports() {} + public @Modifiable @IteratorPolyMod NodeList getImports() {} } diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 10d5043af59e..90e4f6352b9d 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -1,7 +1,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -112,7 +112,7 @@ void testUnmodifiableParamMutation(@UnmodifiableParam List unmodParam, S } void testReplaceAll( - @IteratorPolyShrinkable @Modifiable List mod, @Unmodifiable List unmod) { + @IteratorPolyMod @Modifiable List mod, @Unmodifiable List unmod) { mod.replaceAll(s -> s + "!"); // :: error: [method.invocation] diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 596a02c67d61..eab62642ce4e 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -30,7 +30,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; -import org.checkerframework.checker.modifiability.qual.IteratorPolyShrinkable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -43,7 +43,7 @@ enum TestEnum { B } - @IteratorPolyShrinkable List list; + @IteratorPolyMod List list; void iteratorChecker() { // this should not be allowed because CopyOnWriteArrayList's iterator does not preserve remove @@ -73,7 +73,7 @@ void copyOnWriteArrayListIterator() { List list2 = new CopyOnWriteArrayList<>(); // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: // if the current iterator return unknown (which is what List.iterator() returns), - // then if the receiver is @Shrinkable and has @IteratorPolyShrinkable, then the result is + // then if the receiver is @Shrinkable and has @IteratorPolyMod, then the result is // @Shrinkable. // otherwise, the result is unknown. // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? @@ -133,7 +133,7 @@ void KeySetIterator() { @Shrinkable Iterator iter2 = keys2.iterator(); } - void setIteratorPolyShrinkable() { + void setIteratorPolyMod() { Set hashSet = new HashSet<>(); @Shrinkable Iterator hashSetIterator = hashSet.iterator(); hashSetIterator.remove(); @@ -143,7 +143,7 @@ void setIteratorPolyShrinkable() { treeSetIterator.remove(); } - void dequeIteratorPolyShrinkable() { + void dequeIteratorPolyMod() { Deque arrayDeque = new ArrayDeque<>(); @Shrinkable Iterator arrayDequeIterator = arrayDeque.iterator(); arrayDequeIterator.remove(); @@ -153,7 +153,7 @@ void dequeIteratorPolyShrinkable() { linkedListIterator.remove(); } - void queueIteratorPolyShrinkable() { + void queueIteratorPolyMod() { Queue priorityQueue = new PriorityQueue<>(); @Shrinkable Iterator priorityQueueIterator = priorityQueue.iterator(); priorityQueueIterator.remove(); @@ -171,7 +171,7 @@ void queueIteratorPolyShrinkable() { linkedTransferQueueIterator.remove(); } - void factoryIteratorPolyShrinkable() { + void factoryIteratorPolyMod() { Set enumSet = EnumSet.of(TestEnum.A, TestEnum.B); @Shrinkable Iterator enumSetIterator = enumSet.iterator(); enumSetIterator.remove(); @@ -245,7 +245,7 @@ void unmodifiableMapWrapperViewIteratorsDoNotPreserveRemove() { @Unshrinkable Iterator> entryIterator = map.entrySet().iterator(); } - void iteratorDependentCollectionMethodsRequireIteratorPolyShrinkable( + void iteratorDependentCollectionMethodsRequireIteratorPolyMod( @Modifiable IteratorRemoveUnsupportedCollection collection, @Modifiable IteratorRemoveUnsupportedSet set) { // :: error: [method.invocation] @@ -263,7 +263,7 @@ void iteratorDependentCollectionMethodsRequireIteratorPolyShrinkable( set.removeAll(List.of("a")); } - void iteratorDependentListMethodsRequireIteratorPolyShrinkable( + void iteratorDependentListMethodsRequireIteratorPolyMod( @Modifiable IteratorMutationUnsupportedList list) { // :: error: [method.invocation] list.set(0, "b"); diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index fac922b0eb60..a525f48dca76 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -103,11 +103,11 @@ \begin{verbatim} Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @MaybeGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyShrinkable + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyMod / \ / \ / \ | @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | \ / \ / \ / | - @BottomGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyShrinkable + @BottomGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyMod \end{verbatim} \end{minipage} \end{smaller} @@ -290,13 +290,13 @@ \subsubsectionAndLabel{The Iterator hierarchy}{modifiability-iterator-hierarchy} \begin{description} -\item[\refqualclass{checker/modifiability/qual}{MaybeIteratorPolyShrinkable}] +\item[\refqualclass{checker/modifiability/qual}{MaybeIteratorPolyMod}] The top qualifier. The checker cannot determine whether this collection's \code{iterator()} is \<@PolyShrinkable>. This is the default qualifier for unannotated types. -\item[\refqualclass{checker/modifiability/qual}{IteratorPolyShrinkable}] +\item[\refqualclass{checker/modifiability/qual}{IteratorPolyMod}] This collection's \code{iterator()} is \<@PolyShrinkable>. That is, if collection \ is \<@Shrinkable>, then \ is also \<@Shrinkable>. @@ -416,13 +416,13 @@ \end{Verbatim} A collection uses the second signature if the collection is -\<@IteratorPolyShrinkable>. The collection uses the first signature if the -collection is \<@MaybeIteratorPolyShrinkable>. +\<@IteratorPolyMod>. The collection uses the first signature if the +collection is \<@MaybeIteratorPolyMod>. Here are examples of its use: \begin{Verbatim} - @IteratorPolyShrinkable @Shrinkable List list = ...; + @IteratorPolyMod @Shrinkable List list = ...; @Shrinkable Iterator it = list.iterator(); it.remove(); // OK @@ -502,4 +502,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable -% LocalWords: MaybeIteratorPolyShrinkable IteratorPolyShrinkable +% LocalWords: MaybeIteratorPolyMod IteratorPolyMod From c7f2cfb6bac1b0b23ef3123bbf9bde91f6026db1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 14:46:06 -0700 Subject: [PATCH 189/308] documented IteratorPolyMod --- .../modifiability/qual/IteratorPolyMod.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index 7bddf03970bf..ec4c8d2b8de1 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -8,11 +8,15 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} method - * preserves the ability to call {@code Iterator.remove()}. That is, if collection {@code c} has - * type {@code @Shrinkable}, then {@code c.iterator()} also has type {@code @Shrinkable}. For any - * collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is always - * {@code @Unshrinkable}. + * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} and {@code + * listIterator()} methods preserves the ability to call modification methods in its iterator. + * + *

For example, if collection {@code c} has type {@code @Shrinkable}, then {@code c.iterator()} + * also has type {@code @Shrinkable}. If list {@code l} has type {@code @Replaceable}, then {@code + * l.listIterator()} also has type {@code @Replaceable}. For any collection whose type is + * {@code @MaybeIteratorPolyMod}, its iterator is always {@code @MaybeShrinkable}. + * + *

This annotation is used in GrowChecker, ShrinkChecker, and ReplaceChecker. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ From 93da15d154c1b549f6e9972ffbcb70f1c6dbd902 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 14:52:40 -0700 Subject: [PATCH 190/308] documented ModifiabilityBaseChecker. --- .../modifiability/ModifiabilityBaseChecker.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index cd76ef32cb0f..ff37c180ae40 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -2,7 +2,15 @@ import org.checkerframework.common.basetype.BaseTypeChecker; -/** Base class for the Modifiability sub-checkers. */ +/** + * Base class for the Modifiability sub-checkers. + * + *

This class exists so {@link + * org.checkerframework.framework.source.SourceChecker#getMessagesProperties()} finds the shared + * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The Grow, + * Shrink, and Replace checkers all report diagnostics whose message keys are defined there; without + * this common superclass, those keys would need to be duplicated in each sub-checker's package. + */ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { /** Creates a new ModifiabilityBaseChecker. */ From 4cd731a0060946058adbf4594aae12747af984de Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 15:22:05 -0700 Subject: [PATCH 191/308] clean up refineListIteratorReturnType, delete hasIteratorPolyMod method Co-authored-by: Copilot --- .../grow/GrowAnnotatedTypeFactory.java | 24 ++--------------- .../replace/ReplaceAnnotatedTypeFactory.java | 22 ++-------------- .../shrink/ShrinkAnnotatedTypeFactory.java | 26 +++---------------- 3 files changed, 7 insertions(+), 65 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 129ec92beb98..70dd61b3de31 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -30,7 +30,6 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -214,14 +213,9 @@ private void refineListIteratorReturnType( return; } - if (!receiverType.hasPrimaryAnnotation(GROWABLE)) { - return; - } - - if (hasIteratorPolyMod(receiverType)) { + if (receiverType.hasPrimaryAnnotation(GROWABLE) + && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { returnType.replaceAnnotation(GROWABLE); - } else { - returnType.replaceAnnotation(MAYBE_GROWABLE); } } @@ -248,20 +242,6 @@ private boolean isListIteratorMethod( return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); } - /** - * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. - * - * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation - */ - private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { - if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - return true; - } - return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); - } - /** * Returns true if {@code type} structurally cannot support grow operations. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 3a148dc6607b..dfdd2c050378 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -30,7 +30,6 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -229,11 +228,8 @@ private void refineListIteratorReturnType( return; } - if (!receiverType.hasPrimaryAnnotation(REPLACEABLE)) { - return; - } - - if (hasIteratorPolyMod(receiverType)) { + if (receiverType.hasPrimaryAnnotation(REPLACEABLE) + && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { returnType.replaceAnnotation(REPLACEABLE); } else { returnType.replaceAnnotation(MAYBE_REPLACEABLE); @@ -263,20 +259,6 @@ private boolean isListIteratorMethod( return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); } - /** - * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. - * - * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation - */ - private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { - if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - return true; - } - return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); - } - /** * Returns true if {@code type} structurally cannot support replace operations. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 38af650b3ac6..0f28583bf678 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -31,7 +31,6 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -214,15 +213,10 @@ private void refineIteratorReturnType( returnType.replaceAnnotation(UNSHRINKABLE); } - if (!receiverType.hasPrimaryAnnotation(SHRINKABLE)) { - return; - } - - // receiver type is @Shrinkable. check for @IteratorPolyMod - if (hasIteratorPolyMod(receiverType)) { + // receiver type is @Shrinkable and @IteratorPolyMod + if (receiverType.hasPrimaryAnnotation(SHRINKABLE) + && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { returnType.replaceAnnotation(SHRINKABLE); - } else { - returnType.replaceAnnotation(MAYBE_SHRINKABLE); } } @@ -252,20 +246,6 @@ private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableT return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); } - /** - * Returns true if {@code type} has the {@code @IteratorPolyMod} marker annotation. - * - * @param type the type to test - * @return true if {@code type} has the {@code @IteratorPolyMod} marker annotation - */ - private boolean hasIteratorPolyMod(AnnotatedTypeMirror type) { - if (type.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - return true; - } - return AnnotationUtils.containsSameByClass( - type.getUnderlyingType().getAnnotationMirrors(), IteratorPolyMod.class); - } - /** * Returns true if {@code type} structurally cannot support shrink operations. * From 225ce5496ab8330524ab9884be17e58ff8ae7468 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 15:31:48 -0700 Subject: [PATCH 192/308] document refineListIteratorReturnType --- .../checker/modifiability/grow/GrowAnnotatedTypeFactory.java | 4 ++++ .../modifiability/replace/ReplaceAnnotatedTypeFactory.java | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 70dd61b3de31..0b82f8f26b63 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -196,6 +196,8 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( private void refineListIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror returnType = methodType.getReturnType(); + // Keep explicit ungrowable/growable iterator contracts (for example, CopyOnWriteArrayList, + // ArrayList). if (returnType.hasPrimaryAnnotation(UNGROWABLE) || returnType.hasPrimaryAnnotation(GROWABLE) || returnType.hasPrimaryAnnotation(POLY_GROWABLE)) { @@ -208,11 +210,13 @@ private void refineListIteratorReturnType( } AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + // all ungrowable collections' iterators are ungrowable. if (receiverType.hasPrimaryAnnotation(UNGROWABLE)) { returnType.replaceAnnotation(UNGROWABLE); return; } + // receiver type is @Growable and @IteratorPolyMod if (receiverType.hasPrimaryAnnotation(GROWABLE) && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { returnType.replaceAnnotation(GROWABLE); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index dfdd2c050378..e553231e4aaf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -211,6 +211,8 @@ private void refineCollectionsPlumeWithoutDuplicatesReturnType( private void refineListIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror returnType = methodType.getReturnType(); + // Keep explicit unreplaceable/replaceable iterator contracts (for example, + // CopyOnWriteArrayList, ArrayList). if (returnType.hasPrimaryAnnotation(UNREPLACEABLE) || returnType.hasPrimaryAnnotation(REPLACEABLE) || returnType.hasPrimaryAnnotation(POLY_REPLACEABLE)) { @@ -223,11 +225,12 @@ private void refineListIteratorReturnType( } AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + // all unreplaceable collections' iterators are unreplaceable. if (receiverType.hasPrimaryAnnotation(UNREPLACEABLE)) { returnType.replaceAnnotation(UNREPLACEABLE); return; } - + // receiver type is @Replaceable and @IteratorPolyMod if (receiverType.hasPrimaryAnnotation(REPLACEABLE) && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { returnType.replaceAnnotation(REPLACEABLE); From 2abdd3ac30ed8bc4b0736b29cbf669cd776a8c55 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 15:37:14 -0700 Subject: [PATCH 193/308] fixed isCollectionFromSourceCode documentation Co-authored-by: Copilot --- .../checker/modifiability/ModifiabilityVisitor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 49d51b323e9d..bc521b8c5c95 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -201,11 +201,11 @@ protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} /** - * Returns true if {@code typeElement} is a source-defined subtype of {@link - * java.util.Collection}, {@link java.util.Map}, or {@link java.util.Iterator}. + * Returns true if {@code typeElement} is a user-defined subtype of {@link java.util.Collection}, + * {@link java.util.Map}, or {@link java.util.Iterator}. * * @param typeElement the type element to test - * @return true if this source-defined type relies on trusted custom modifiability annotations + * @return true if {@code typeElement} is a user-defined subtype of Collection, Map, or Iterator */ private boolean isCollectionFromSourceCode(TypeElement typeElement) { if (!ElementUtils.isElementFromSourceCode(typeElement)) { From eb1be9f2729835ca061e5ff7354e9ce56250640e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 16:40:42 -0700 Subject: [PATCH 194/308] fixed canonicalAnnotation, all modifiabilityTest passed. --- .../checker/modifiability/grow/GrowAnnotatedTypeFactory.java | 3 ++- .../modifiability/replace/ReplaceAnnotatedTypeFactory.java | 3 ++- .../modifiability/shrink/ShrinkAnnotatedTypeFactory.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 0b82f8f26b63..39f610365b8a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -125,7 +125,8 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, PolyModifiable.class)) { return isMapEntry(tm) ? MAYBE_GROWABLE : POLY_GROWABLE; } - } else if (areSameByClass(annotation, MaybeModifiable.class) + } + if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_GROWABLE; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index e553231e4aaf..528d88784f14 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -138,7 +138,8 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, Unmodifiable.class)) { return typeCannotReplace(tm) ? MAYBE_REPLACEABLE : UNREPLACEABLE; } - } else if (areSameByClass(annotation, MaybeModifiable.class) + } + if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_REPLACEABLE; } else if (areSameByClass(annotation, PolyModifiable.class)) { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 0f28583bf678..3efdd2ba8161 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -122,7 +122,8 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, PolyModifiable.class)) { return isMapEntry(tm) ? MAYBE_SHRINKABLE : POLY_SHRINKABLE; } - } else if (areSameByClass(annotation, MaybeModifiable.class) + } + if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_SHRINKABLE; } From a738f26754da2863f44fe9d06065d9014dfc6ef7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 29 May 2026 23:31:30 -0700 Subject: [PATCH 195/308] created PreservesModifiability annotated and annotated plume-util.withoutDuplicate. Co-authored-by: Copilot --- .../qual/PreservesModifiability.java | 25 +++++++++ .../ModifiabilityMethodUtils.java | 42 --------------- .../grow/GrowAnnotatedTypeFactory.java | 35 +++++++------ .../replace/ReplaceAnnotatedTypeFactory.java | 35 +++++++------ .../shrink/ShrinkAnnotatedTypeFactory.java | 35 +++++++------ .../PreservesModifiabilityTest.java | 52 +++++++++++++++++++ docs/manual/modifiability-checker.tex | 18 ++++++- 7 files changed, 153 insertions(+), 89 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java create mode 100644 checker/tests/modifiability/PreservesModifiabilityTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java new file mode 100644 index 000000000000..5bbcc4f6e677 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * The annotated method preserves modifiability capabilities from its first argument to its return + * value. + * + *

For example, if the first argument is {@code @Modifiable} or {@code @IteratorPolyMod}, then + * the return type is also {@code @Modifiable} or {@code @IteratorPolyMod}. If the first argument is + * anything other than {@code @Modifiable} or {@code @IteratorPolyMod}, then the return type is the + * top {@code @MaybeModifiable}. + * + *

If the annotated method has no parameters, then this annotation has no effect on the method. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface PreservesModifiability {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java deleted file mode 100644 index abc8e8cee8ff..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityMethodUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.checkerframework.checker.modifiability; - -import com.sun.source.tree.MethodInvocationTree; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import org.checkerframework.javacutil.TreeUtils; - -/** Utility methods for Modifiability checker method-specific behavior. */ -public final class ModifiabilityMethodUtils { - - /** The fully-qualified class name of plume-util's collections utilities. */ - private static final String COLLECTIONS_PLUME = "org.plumelib.util.CollectionsPlume"; - - /** The method name in CollectionsPlume for removing duplicates from a collection. */ - private static final String WITHOUT_DUPLICATES = "withoutDuplicates"; - - /** This is a utility class. */ - private ModifiabilityMethodUtils() { - throw new Error("Do not instantiate"); - } - - /** - * Returns true if {@code tree} is a call to {@code - * org.plumelib.util.CollectionsPlume.withoutDuplicates(Collection)}. - * - * @param tree a method invocation tree - * @return true iff {@code tree} invokes {@code CollectionsPlume.withoutDuplicates(Collection)} - */ - public static boolean isCollectionsPlumeWithoutDuplicates(MethodInvocationTree tree) { - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod != null - && invokedMethod.getSimpleName().contentEquals(WITHOUT_DUPLICATES) - && invokedMethod.getParameters().size() == 1) { - Element enclosingElement = invokedMethod.getEnclosingElement(); - if (enclosingElement instanceof TypeElement enclosingType) { - return enclosingType.getQualifiedName().contentEquals(COLLECTIONS_PLUME); - } - } - return false; - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 39f610365b8a..248894109c4a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -11,7 +11,6 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; @@ -21,6 +20,7 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrowable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; @@ -143,33 +143,38 @@ protected ParameterizedExecutableType methodFromUse( refineListIteratorReturnType(tree, method); } - if (ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { - refineCollectionsPlumeWithoutDuplicatesReturnType(tree, method); + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refinePreservesModifiabilityReturnType(tree, method); } return mType; } /** - * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. + * Refines the return type of a {@code @PreservesModifiability} method. + * + *

If the method has no parameters, then this annotation has no effect. * - *

{@code withoutDuplicates} returns its argument or a new modifiable ArrayList. The signature + *

Otherwise, if the first argument is {@code @Growable}, then the return type is also + * {@code @Growable}. If the first argument is {@code @IteratorPolyMod}, then the return type is + * also {@code @IteratorPolyMod}. * - *

{@code
-   * static  @PolyGrowable List withoutDuplicates(@PolyGrowable Collection values)
-   * }
+ *

For every other case, the return type is {@code @MaybeGrowable}. * - * would be unsound, because an {@code @Ungrowable} input could yield a growable ArrayList, whose - * static type should be {@code @Growable}. It would be sound but imprecise to always use - * {@code @MaybeGrowable}, because passing a {@code @Growable} collection guarantees that both - * possible results are growable. Therefore, model the method here as preserving {@code @Growable} - * inputs and otherwise returning {@code @MaybeGrowable}. + *

Such method cannot be annotated as {@code @PolyGrowable} because an {@code @Ungrowable} + * input could yield either a growable or ungrowable result. It would be imprecise to always use + * {@code @MaybeGrowable}, because passing a {@code @Growable} collection guarantees that + * {@code @Growable} return type. * - * @param tree an invocation of {@code withoutDuplicates} + * @param tree an invocation of a {@code @PreservesModifiability} method * @param methodType the annotated executable type of the invoked method */ - private void refineCollectionsPlumeWithoutDuplicatesReturnType( + private void refinePreservesModifiabilityReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (tree.getArguments().isEmpty()) { + return; + } AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(GROWABLE)) { methodType.getReturnType().replaceAnnotation(GROWABLE); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 528d88784f14..3f830ea16e45 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -11,7 +11,6 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -20,6 +19,7 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplaceable; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; @@ -158,33 +158,38 @@ protected ParameterizedExecutableType methodFromUse( refineListIteratorReturnType(tree, method); } - if (ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { - refineCollectionsPlumeWithoutDuplicatesReturnType(tree, method); + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refinePreservesModifiabilityReturnType(tree, method); } return mType; } /** - * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. + * Refines the return type of a {@code @PreservesModifiability} method. + * + *

If the method has no parameters, then this annotation has no effect. * - *

{@code withoutDuplicates} returns its argument or a new modifiable ArrayList. The signature + *

Otherwise, if the first argument is {@code @Replaceable}, then the return type is also + * {@code @Replaceable}. If the first argument is {@code @IteratorPolyMod}, then the return type + * is also {@code @IteratorPolyMod}. * - *

{@code
-   * static  @PolyReplaceable List withoutDuplicates(@PolyReplaceable Collection values)
-   * }
+ *

For every other case, the return type is {@code @MaybeReplaceable}. * - * would be unsound, because an {@code @Unreplaceable} input could yield a replaceable ArrayList, - * whose static type should be {@code @Replaceable}. It would be sound but imprecise to always use - * {@code @MaybeReplaceable}, because passing a {@code @Replaceable} collection guarantees that - * both possible results are replaceable. Therefore, model the method here as preserving - * {@code @Replaceable} inputs and otherwise returning {@code @MaybeReplaceable}. + *

Such method cannot be annotated as {@code @PolyReplaceable} because an + * {@code @Unreplaceable} input could yield either a replaceable or unreplaceable result. It would + * be imprecise to always use {@code @MaybeReplaceable}, because passing a {@code @Replaceable} + * collection guarantees {@code @Replaceable} return type. * - * @param tree an invocation of {@code withoutDuplicates} + * @param tree an invocation of a {@code @PreservesModifiability} method * @param methodType the annotated executable type of the invoked method */ - private void refineCollectionsPlumeWithoutDuplicatesReturnType( + private void refinePreservesModifiabilityReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (tree.getArguments().isEmpty()) { + return; + } AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(REPLACEABLE)) { methodType.getReturnType().replaceAnnotation(REPLACEABLE); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 3efdd2ba8161..fe6daac8cf9a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -11,7 +11,6 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -import org.checkerframework.checker.modifiability.ModifiabilityMethodUtils; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -20,6 +19,7 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; @@ -140,33 +140,38 @@ protected ParameterizedExecutableType methodFromUse( refineIteratorReturnType(tree, method); } - if (ModifiabilityMethodUtils.isCollectionsPlumeWithoutDuplicates(tree)) { - refineCollectionsPlumeWithoutDuplicatesReturnType(tree, method); + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refinePreservesModifiabilityReturnType(tree, method); } return mType; } /** - * Refines the return type of {@code CollectionsPlume.withoutDuplicates(Collection)}. + * Refines the return type of a {@code @PreservesModifiability} method. + * + *

If the method has no parameters, then this annotation has no effect. * - *

{@code withoutDuplicates} returns its argument or a new modifiable ArrayList. The signature + *

Otherwise, if the first argument is {@code @Shrinkable}, then the return type is also + * {@code @Shrinkable}. If the first argument is {@code @IteratorPolyMod}, then the return type is + * also {@code @IteratorPolyMod}. * - *

{@code
-   * static  @PolyShrinkable List withoutDuplicates(@PolyShrinkable Collection values)
-   * }
+ *

For every other case, the return type is {@code @MaybeShrinkable}. * - * would be unsound, because an {@code @Unshrinkable} input could yield a shrinkable ArrayList, - * whose static type should be {@code @Shrinkable}. It would be sound but imprecise to always use - * {@code @MaybeShrinkable}, because passing a {@code @Shrinkable} collection guarantees that both - * possible results are shrinkable. Therefore, model the method here as preserving - * {@code @Shrinkable} inputs and otherwise returning {@code @MaybeShrinkable}. + *

Such method cannot be annotated as {@code @PolyShrinkable} because an {@code @Unshrinkable} + * input could yield either a shrinkable or unshrinkable result. It would be imprecise to always + * use {@code @MaybeShrinkable}, because passing a {@code @Shrinkable} collection guarantees that + * {@code @Shrinkable} return type. * - * @param tree an invocation of {@code withoutDuplicates} + * @param tree an invocation of a {@code @PreservesModifiability} method * @param methodType the annotated executable type of the invoked method */ - private void refineCollectionsPlumeWithoutDuplicatesReturnType( + private void refinePreservesModifiabilityReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (tree.getArguments().isEmpty()) { + return; + } AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(SHRINKABLE)) { methodType.getReturnType().replaceAnnotation(SHRINKABLE); diff --git a/checker/tests/modifiability/PreservesModifiabilityTest.java b/checker/tests/modifiability/PreservesModifiabilityTest.java new file mode 100644 index 000000000000..6f29c934fc8d --- /dev/null +++ b/checker/tests/modifiability/PreservesModifiabilityTest.java @@ -0,0 +1,52 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; + +class PreservesModifiabilityTest { + + @PreservesModifiability + static List annotated(Collection values) { + return new ArrayList<>(values); + } + + static List unannotated(Collection values) { + return new ArrayList<>(values); + } + + void preservesCapabilities( + @Growable List growable, + @Shrinkable List shrinkable, + @Replaceable List replaceable, + @IteratorPolyMod List iteratorPoly, + @Modifiable List modifiable) { + @Growable List g = annotated(growable); + @Shrinkable List s = annotated(shrinkable); + @Replaceable List r = annotated(replaceable); + @IteratorPolyMod List i = annotated(iteratorPoly); + @Modifiable List m = annotated(modifiable); + } + + void unannotatedDoesNotPreserve( + @Growable List growable, + @Shrinkable List shrinkable, + @Replaceable List replaceable, + @IteratorPolyMod List iteratorPoly, + @Modifiable List modifiable) { + // :: error: [assignment] + @Growable List g = unannotated(growable); + // :: error: [assignment] + @Shrinkable List s = unannotated(shrinkable); + // :: error: [assignment] + @Replaceable List r = unannotated(replaceable); + // :: error: [assignment] + @IteratorPolyMod List i = unannotated(iteratorPoly); + // :: error: [assignment] + @Modifiable List m = unannotated(modifiable); + } +} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index a525f48dca76..d8868bb601ab 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -341,7 +341,7 @@ \subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} -The Modifiability Checker supports one annotation that specifies method +The Modifiability Checker supports method annotations that specify method behavior. \begin{description} @@ -353,6 +353,20 @@ erroneous. The Modifiability Checker issues an error at every call to a method annotated with \code{@ThrowsUOE}. +\item[\refqualclass{checker/modifiability/qual}{PreservesModifiability}] +When written on a one-argument method declaration, it indicates that the +method preserves positive modifiability capabilities from its argument to +its return value. If the argument is \<@Growable>, \<@Shrinkable>, +\<@Replaceable>, or \<@IteratorPolyMod>, then the Modifiability Checker +treats the return value as having that same capability. If the argument has +any other qualifier, the return type is the top qualifier (\<@MaybeGrowable>, +\<@MaybeShrinkable>, \<@MaybeReplaceable>, or \<@MaybeIteratorPolyMod>) in +the corresponding hierarchy. + +This is useful for methods that may return their argument, or may return a +new modifiable collection. + +If put on a method with no parameters, then it has no effect. \end{description} \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} @@ -502,4 +516,4 @@ % LocalWords: addLast unreplaceable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable -% LocalWords: MaybeIteratorPolyMod IteratorPolyMod +% LocalWords: MaybeIteratorPolyMod IteratorPolyMod PreservesModifiability From 87354ec1cf1d546e9f6b65ec3601df40c47cfb39 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 30 May 2026 16:50:26 -0700 Subject: [PATCH 196/308] refactored common methods into ModifiabilityAnnotatedTypeFactory. Co-authored-by: Copilot --- .../ModifiabilityAnnotatedTypeFactory.java | 192 ++++++++++++++++++ .../grow/GrowAnnotatedTypeFactory.java | 137 +++---------- .../replace/ReplaceAnnotatedTypeFactory.java | 136 +++---------- .../shrink/ShrinkAnnotatedTypeFactory.java | 141 +++---------- 4 files changed, 276 insertions(+), 330 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java new file mode 100644 index 000000000000..9c21d5aabb5a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -0,0 +1,192 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +/** Shared annotated type factory logic for the Modifiability sub-checkers. */ +public abstract class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; + + /** The erased {@code java.util.ListIterator} type. */ + private final TypeMirror listIteratorErasure; + + /** + * Creates a ModifiabilityAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + + Types types = getProcessingEnv().getTypeUtils(); + this.iteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + this.listIteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); + } + + /** + * Refines the return type of a {@code @PreservesModifiability} method. + * + *

If the method has no parameters, then this annotation has no effect. + * + *

Otherwise, if the first argument is positive (i.e.{@code @Shrinkable}), then the return type + * is also positive. If the first argument is {@code @IteratorPolyMod}, then the return type is + * also {@code @IteratorPolyMod}. + * + *

For every other case, the return type is top ({@code @Maybe*}). + * + *

Such method cannot be annotated as {@code @Poly*} because an negative (e.g. + * {@code @Unshrinkable}) input could yield either a shrinkable or unshrinkable result. It would + * be imprecise to always use {@code @Maybe*}, because passing a positive type collection + * guarantees a positive return type. + * + *

This method is called by all three sub-checkers. + * + * @param tree an invocation of a {@code @PreservesModifiability} method + * @param methodType the annotated executable type of the invoked method + */ + protected void refinePreservesModifiabilityReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (tree.getArguments().isEmpty()) { + return; + } + AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); + if (argumentType.hasPrimaryAnnotation(positiveCapability())) { + methodType.getReturnType().replaceAnnotation(positiveCapability()); + } + if (argumentType.hasPrimaryAnnotation(iteratorPreserveRemove())) { + methodType.getReturnType().replaceAnnotation(iteratorPreserveRemove()); + } + } + + /** + * Refines {@code iterator()} and {@code listIterator()} return types based on + * {@code @IteratorPolyMod}. + * + *

{@code iterator()} and {@code listIterator()} cannot be annotated as {@code @PolyModifiable} + * because not all collections preserve the modifiability of their iterators. (For example, {@code + * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, + * special treatment is needed for Iterator methods. + * + *

If the receiver is {@code @Modifiable} and {@code @IteratorPolyMod}, then the result is + * {@code @Modifiable Iterator}. Otherwise, shrinkability precision is dropped to + * {@code @MaybeModifiable}. + * + *

This method is called by all three sub-checkers. In ShrinkAnnotatedTypeFactory, this method + * is called by both {@code iterator()} and {@code listIterator()} methods. In + * GrowAnnotatedTypeFactory and ReplaceAnnotatedTypeFactory, this method is only called by {@code + * listIterator()} methods. + * + * @param tree the iterator method invocation + * @param methodType the annotated executable type of the invoked method + */ + protected void refineIteratorReturnType( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + AnnotatedTypeMirror returnType = methodType.getReturnType(); + // Keep explicit ungrowable/growable iterator contracts (for example, CopyOnWriteArrayList, + // ArrayList). + if (returnType.hasPrimaryAnnotation(negativeCapability()) + || returnType.hasPrimaryAnnotation(positiveCapability()) + || returnType.hasPrimaryAnnotation(polyCapability())) { + return; + } + + Tree receiverTree = TreeUtils.getReceiverTree(tree); + if (receiverTree == null) { + return; + } + AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); + + // all ungrowable collections' iterators are ungrowable. + if (receiverType.hasPrimaryAnnotation(negativeCapability())) { + returnType.replaceAnnotation(negativeCapability()); + return; + } + + // receiver type is @Modifiable and @IteratorPolyMod + if (receiverType.hasPrimaryAnnotation(positiveCapability()) + && receiverType.hasPrimaryAnnotation(iteratorPreserveRemove())) { + returnType.replaceAnnotation(positiveCapability()); + } + } + + /** + * Returns true if this invocation is a {@code listIterator()} method that returns a ListIterator. + * + * @param tree the method invocation to test + * @param methodType the annotated executable type of the invoked method + * @return true if this invocation returns a ListIterator from {@code listIterator()} + */ + protected boolean isListIteratorMethod( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null) { + return false; + } + // quick syntax check before expensive erasure checks + if (!invokedMethod.getSimpleName().contentEquals("listIterator") + || tree.getArguments().size() > 1) { + return false; + } + // Check if the return type is an erased ListIterator + TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); + return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); + } + + /** + * Returns true if this invocation is an iterator-returning {@code iterator()} or {@code + * listIterator()} method. + * + * @param tree the method invocation to test + * @param methodType the annotated executable type of the invoked method + * @return true if this invocation returns an Iterator from {@code iterator()} or {@code + * listIterator()} + */ + protected boolean isIteratorMethod( + MethodInvocationTree tree, AnnotatedExecutableType methodType) { + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (invokedMethod == null) { + return false; + } + // quick syntax check before expensive erasure checks + String methodName = invokedMethod.getSimpleName().toString(); + int argCount = tree.getArguments().size(); + if (!((methodName.equals("iterator") && argCount == 0) + || (methodName.equals("listIterator") && argCount <= 1))) { + return false; + } + // Check if the return type is an erased Iterator + TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); + return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); + } + + /** Returns the top/unknown qualifier. */ + protected abstract AnnotationMirror maybeCapability(); + + /** Returns the positive capability qualifier. */ + protected abstract AnnotationMirror positiveCapability(); + + /** Returns the negative capability qualifier. */ + protected abstract AnnotationMirror negativeCapability(); + + /** Returns the polymorphic capability qualifier. */ + protected abstract AnnotationMirror polyCapability(); + + /** Returns the {@code @}{@link IteratorPolyMod} qualifier. */ + protected abstract AnnotationMirror iteratorPreserveRemove(); +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 248894109c4a..dd89a037e3bf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability.grow; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -11,6 +10,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; @@ -25,16 +25,14 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link GrowChecker}. */ -public class GrowAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class GrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryErasure; @@ -89,6 +87,31 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } + @Override + protected AnnotationMirror maybeCapability() { + return MAYBE_GROWABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return GROWABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return UNGROWABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_GROWABLE; + } + + @Override + protected AnnotationMirror iteratorPreserveRemove() { + return ITERATOR_PRESERVE_REMOVE; + } + @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( @@ -140,7 +163,7 @@ protected ParameterizedExecutableType methodFromUse( AnnotatedExecutableType method = mType.executableType(); if (isListIteratorMethod(tree, method)) { - refineListIteratorReturnType(tree, method); + refineIteratorReturnType(tree, method); } ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); @@ -151,107 +174,6 @@ protected ParameterizedExecutableType methodFromUse( return mType; } - /** - * Refines the return type of a {@code @PreservesModifiability} method. - * - *

If the method has no parameters, then this annotation has no effect. - * - *

Otherwise, if the first argument is {@code @Growable}, then the return type is also - * {@code @Growable}. If the first argument is {@code @IteratorPolyMod}, then the return type is - * also {@code @IteratorPolyMod}. - * - *

For every other case, the return type is {@code @MaybeGrowable}. - * - *

Such method cannot be annotated as {@code @PolyGrowable} because an {@code @Ungrowable} - * input could yield either a growable or ungrowable result. It would be imprecise to always use - * {@code @MaybeGrowable}, because passing a {@code @Growable} collection guarantees that - * {@code @Growable} return type. - * - * @param tree an invocation of a {@code @PreservesModifiability} method - * @param methodType the annotated executable type of the invoked method - */ - private void refinePreservesModifiabilityReturnType( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - if (tree.getArguments().isEmpty()) { - return; - } - AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); - if (argumentType.hasPrimaryAnnotation(GROWABLE)) { - methodType.getReturnType().replaceAnnotation(GROWABLE); - } - if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); - } - } - - /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. - * - *

{@code listIterator()} cannot be annotated as {@code @PolyModifiable} because not all - * collections preserve the modifiability of their iterators. (For example, {@code - * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, - * special treatment is needed for Iterator methods. - * - *

If the receiver is {@code @Growable} and {@code @IteratorPolyMod}, then the result is - * {@code @Growable Iterator}. Otherwise, growability precision is dropped to - * {@code @MaybeGrowable}. - * - * @param tree the listIterator method invocation - * @param methodType the annotated executable type of the invoked method - */ - private void refineListIteratorReturnType( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - AnnotatedTypeMirror returnType = methodType.getReturnType(); - // Keep explicit ungrowable/growable iterator contracts (for example, CopyOnWriteArrayList, - // ArrayList). - if (returnType.hasPrimaryAnnotation(UNGROWABLE) - || returnType.hasPrimaryAnnotation(GROWABLE) - || returnType.hasPrimaryAnnotation(POLY_GROWABLE)) { - return; - } - - Tree receiverTree = TreeUtils.getReceiverTree(tree); - if (receiverTree == null) { - return; - } - AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); - - // all ungrowable collections' iterators are ungrowable. - if (receiverType.hasPrimaryAnnotation(UNGROWABLE)) { - returnType.replaceAnnotation(UNGROWABLE); - return; - } - - // receiver type is @Growable and @IteratorPolyMod - if (receiverType.hasPrimaryAnnotation(GROWABLE) - && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - returnType.replaceAnnotation(GROWABLE); - } - } - - /** - * Returns true if this invocation is a {@code listIterator()} method that returns an Iterator. - * - * @param tree the method invocation to test - * @param methodType the annotated executable type of the invoked method - * @return true if this invocation returns an Iterator from {@code listIterator()} - */ - private boolean isListIteratorMethod( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null) { - return false; - } - // quick syntax check before expensive erasure checks - if (!invokedMethod.getSimpleName().contentEquals("listIterator") - || tree.getArguments().size() > 1) { - return false; - } - // Check if the return type is an erased ListIterator - TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); - } - /** * Returns true if {@code type} structurally cannot support grow operations. * @@ -274,9 +196,6 @@ private boolean typeCannotGrow(TypeMirror type) { * @return true if {@code type} is a subtype of {@link java.util.Map.Entry} */ private boolean isMapEntry(TypeMirror type) { - if (type.getKind() != TypeKind.DECLARED) { - return false; - } return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 3f830ea16e45..7ce9f73874d1 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability.replace; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -11,6 +10,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -25,16 +25,15 @@ import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ReplaceChecker}. */ -public class ReplaceAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class ReplaceAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { /** The erased {@code java.util.Set} type. */ private final TypeMirror setErasure; @@ -103,6 +102,31 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } + @Override + protected AnnotationMirror maybeCapability() { + return MAYBE_REPLACEABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return REPLACEABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return UNREPLACEABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_REPLACEABLE; + } + + @Override + protected AnnotationMirror iteratorPreserveRemove() { + return ITERATOR_PRESERVE_REMOVE; + } + @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( @@ -155,7 +179,7 @@ protected ParameterizedExecutableType methodFromUse( AnnotatedExecutableType method = mType.executableType(); if (isListIteratorMethod(tree, method)) { - refineListIteratorReturnType(tree, method); + refineIteratorReturnType(tree, method); } ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); @@ -166,108 +190,6 @@ protected ParameterizedExecutableType methodFromUse( return mType; } - /** - * Refines the return type of a {@code @PreservesModifiability} method. - * - *

If the method has no parameters, then this annotation has no effect. - * - *

Otherwise, if the first argument is {@code @Replaceable}, then the return type is also - * {@code @Replaceable}. If the first argument is {@code @IteratorPolyMod}, then the return type - * is also {@code @IteratorPolyMod}. - * - *

For every other case, the return type is {@code @MaybeReplaceable}. - * - *

Such method cannot be annotated as {@code @PolyReplaceable} because an - * {@code @Unreplaceable} input could yield either a replaceable or unreplaceable result. It would - * be imprecise to always use {@code @MaybeReplaceable}, because passing a {@code @Replaceable} - * collection guarantees {@code @Replaceable} return type. - * - * @param tree an invocation of a {@code @PreservesModifiability} method - * @param methodType the annotated executable type of the invoked method - */ - private void refinePreservesModifiabilityReturnType( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - if (tree.getArguments().isEmpty()) { - return; - } - AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); - if (argumentType.hasPrimaryAnnotation(REPLACEABLE)) { - methodType.getReturnType().replaceAnnotation(REPLACEABLE); - } - if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); - } - } - - /** - * Refines {@code listIterator()} return type based on {@code @IteratorPolyMod}. - * - *

{@code listIterator()} cannot be annotated as {@code @PolyModifiable} because not all - * collections preserve the modifiability of their iterators. (For example, {@code - * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, - * special treatment is needed for Iterator methods. - * - *

If the receiver is {@code @Replaceable} and {@code @IteratorPolyMod}, then the result is - * {@code @Replaceable Iterator}. Otherwise, replaceability precision is dropped to - * {@code @MaybeReplaceable}. - * - * @param tree the listIterator method invocation - * @param methodType the annotated executable type of the invoked method - */ - private void refineListIteratorReturnType( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - AnnotatedTypeMirror returnType = methodType.getReturnType(); - // Keep explicit unreplaceable/replaceable iterator contracts (for example, - // CopyOnWriteArrayList, ArrayList). - if (returnType.hasPrimaryAnnotation(UNREPLACEABLE) - || returnType.hasPrimaryAnnotation(REPLACEABLE) - || returnType.hasPrimaryAnnotation(POLY_REPLACEABLE)) { - return; - } - - Tree receiverTree = TreeUtils.getReceiverTree(tree); - if (receiverTree == null) { - return; - } - AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); - - // all unreplaceable collections' iterators are unreplaceable. - if (receiverType.hasPrimaryAnnotation(UNREPLACEABLE)) { - returnType.replaceAnnotation(UNREPLACEABLE); - return; - } - // receiver type is @Replaceable and @IteratorPolyMod - if (receiverType.hasPrimaryAnnotation(REPLACEABLE) - && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - returnType.replaceAnnotation(REPLACEABLE); - } else { - returnType.replaceAnnotation(MAYBE_REPLACEABLE); - } - } - - /** - * Returns true if this invocation is a {@code listIterator()} method that returns an Iterator. - * - * @param tree the method invocation to test - * @param methodType the annotated executable type of the invoked method - * @return true if this invocation returns an Iterator from {@code listIterator()} - */ - private boolean isListIteratorMethod( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null) { - return false; - } - // quick syntax check before expensive erasure checks - if (!invokedMethod.getSimpleName().contentEquals("listIterator") - || tree.getArguments().size() > 1) { - return false; - } - // Check if the return type is an erased ListIterator - TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); - } - /** * Returns true if {@code type} structurally cannot support replace operations. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index fe6daac8cf9a..79c5f04187de 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability.shrink; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -11,6 +10,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -25,24 +25,18 @@ import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; -import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ShrinkChecker}. */ -public class ShrinkAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class ShrinkAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryErasure; - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - // -- Hierarchy qualifiers ---------- /** The {@code @}{@link MaybeShrinkable} qualifier. */ @@ -72,8 +66,6 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { Types types = getProcessingEnv().getTypeUtils(); this.mapEntryErasure = types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); - this.iteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. this.MAYBE_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrinkable.class); @@ -86,6 +78,31 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } + @Override + protected AnnotationMirror maybeCapability() { + return MAYBE_SHRINKABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return SHRINKABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return UNSHRINKABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_SHRINKABLE; + } + + @Override + protected AnnotationMirror iteratorPreserveRemove() { + return ITERATOR_PRESERVE_REMOVE; + } + @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( @@ -148,110 +165,6 @@ protected ParameterizedExecutableType methodFromUse( return mType; } - /** - * Refines the return type of a {@code @PreservesModifiability} method. - * - *

If the method has no parameters, then this annotation has no effect. - * - *

Otherwise, if the first argument is {@code @Shrinkable}, then the return type is also - * {@code @Shrinkable}. If the first argument is {@code @IteratorPolyMod}, then the return type is - * also {@code @IteratorPolyMod}. - * - *

For every other case, the return type is {@code @MaybeShrinkable}. - * - *

Such method cannot be annotated as {@code @PolyShrinkable} because an {@code @Unshrinkable} - * input could yield either a shrinkable or unshrinkable result. It would be imprecise to always - * use {@code @MaybeShrinkable}, because passing a {@code @Shrinkable} collection guarantees that - * {@code @Shrinkable} return type. - * - * @param tree an invocation of a {@code @PreservesModifiability} method - * @param methodType the annotated executable type of the invoked method - */ - private void refinePreservesModifiabilityReturnType( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - if (tree.getArguments().isEmpty()) { - return; - } - AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); - if (argumentType.hasPrimaryAnnotation(SHRINKABLE)) { - methodType.getReturnType().replaceAnnotation(SHRINKABLE); - } - if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); - } - } - - /** - * Refines {@code iterator()} and {@code listIterator()} return types based on - * {@code @IteratorPolyMod}. - * - *

{@code iterator()} and {@code listIterator()} cannot be annotated as {@code @PolyModifiable} - * because not all collections preserve the modifiability of their iterators. (For example, {@code - * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, - * special treatment is needed for Iterator methods. - * - *

If the receiver is {@code @Shrinkable} and {@code @IteratorPolyMod}, then the result is - * {@code @Shrinkable Iterator}. Otherwise, shrinkability precision is dropped to - * {@code @MaybeShrinkable}. - * - * @param tree the iterator method invocation - * @param methodType the annotated executable type of the invoked method - */ - private void refineIteratorReturnType( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - AnnotatedTypeMirror returnType = methodType.getReturnType(); - // Keep explicit unshrinkable/shrinkable iterator contracts (for example, CopyOnWriteArrayList, - // ArrayList). - if (returnType.hasPrimaryAnnotation(UNSHRINKABLE) - || returnType.hasPrimaryAnnotation(SHRINKABLE) - || returnType.hasPrimaryAnnotation(POLY_SHRINKABLE)) { - return; - } - - Tree receiverTree = TreeUtils.getReceiverTree(tree); - if (receiverTree == null) { - return; - } - AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); - - // all unshrinkable collections' iterators are unshrinkable. - if (receiverType.hasPrimaryAnnotation(UNSHRINKABLE)) { - returnType.replaceAnnotation(UNSHRINKABLE); - } - - // receiver type is @Shrinkable and @IteratorPolyMod - if (receiverType.hasPrimaryAnnotation(SHRINKABLE) - && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - returnType.replaceAnnotation(SHRINKABLE); - } - } - - /** - * Returns true if this invocation is an iterator-returning {@code iterator()} or {@code - * listIterator()} method. - * - * @param tree the method invocation to test - * @param methodType the annotated executable type of the invoked method - * @return true if this invocation returns an Iterator from {@code iterator()} or {@code - * listIterator()} - */ - private boolean isIteratorMethod(MethodInvocationTree tree, AnnotatedExecutableType methodType) { - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null) { - return false; - } - // quick syntax check before expensive erasure checks - String methodName = invokedMethod.getSimpleName().toString(); - int argCount = tree.getArguments().size(); - if (!((methodName.equals("iterator") && argCount == 0) - || (methodName.equals("listIterator") && argCount <= 1))) { - return false; - } - // Check if the return type is an erased Iterator - TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); - } - /** * Returns true if {@code type} structurally cannot support shrink operations. * From 2eca59c7c48884dff02d53b6c9045c91c3a16047 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 30 May 2026 17:10:30 -0700 Subject: [PATCH 197/308] bring iterator_preserve_remove to modifiabilityAnnotatedTypeFactory --- .../ModifiabilityAnnotatedTypeFactory.java | 15 +++++++++------ .../grow/GrowAnnotatedTypeFactory.java | 10 ---------- .../replace/ReplaceAnnotatedTypeFactory.java | 10 ---------- .../shrink/ShrinkAnnotatedTypeFactory.java | 10 ---------- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 9c21d5aabb5a..9616d70b535e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -11,6 +11,7 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -23,6 +24,9 @@ public abstract class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTyp /** The erased {@code java.util.ListIterator} type. */ private final TypeMirror listIteratorErasure; + /** The {@code @}{@link IteratorPolyMod} qualifier. */ + protected final AnnotationMirror ITERATOR_PRESERVE_REMOVE; + /** * Creates a ModifiabilityAnnotatedTypeFactory. * @@ -37,6 +41,8 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); this.listIteratorErasure = types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); + this.ITERATOR_PRESERVE_REMOVE = + AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); } /** @@ -69,8 +75,8 @@ protected void refinePreservesModifiabilityReturnType( if (argumentType.hasPrimaryAnnotation(positiveCapability())) { methodType.getReturnType().replaceAnnotation(positiveCapability()); } - if (argumentType.hasPrimaryAnnotation(iteratorPreserveRemove())) { - methodType.getReturnType().replaceAnnotation(iteratorPreserveRemove()); + if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); } } @@ -120,7 +126,7 @@ protected void refineIteratorReturnType( // receiver type is @Modifiable and @IteratorPolyMod if (receiverType.hasPrimaryAnnotation(positiveCapability()) - && receiverType.hasPrimaryAnnotation(iteratorPreserveRemove())) { + && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { returnType.replaceAnnotation(positiveCapability()); } } @@ -186,7 +192,4 @@ protected boolean isIteratorMethod( /** Returns the polymorphic capability qualifier. */ protected abstract AnnotationMirror polyCapability(); - - /** Returns the {@code @}{@link IteratorPolyMod} qualifier. */ - protected abstract AnnotationMirror iteratorPreserveRemove(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index dd89a037e3bf..1af181101ce1 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -57,9 +57,6 @@ public class GrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory /** The {@code @}{@link PolyGrowable} qualifier. */ private final AnnotationMirror POLY_GROWABLE; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ - private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; - /** * Creates a GrowAnnotatedTypeFactory. * @@ -81,8 +78,6 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); this.POLY_GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyGrowable.class); - this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); postInit(); } @@ -107,11 +102,6 @@ protected AnnotationMirror polyCapability() { return POLY_GROWABLE; } - @Override - protected AnnotationMirror iteratorPreserveRemove() { - return ITERATOR_PRESERVE_REMOVE; - } - @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 7ce9f73874d1..dcec1177b258 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -67,9 +67,6 @@ public class ReplaceAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFacto /** The {@code @}{@link PolyReplaceable} qualifier. */ private final AnnotationMirror POLY_REPLACEABLE; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ - private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; - /** * Creates a ReplaceAnnotatedTypeFactory. * @@ -96,8 +93,6 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplaceable.class); - this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); postInit(); } @@ -122,11 +117,6 @@ protected AnnotationMirror polyCapability() { return POLY_REPLACEABLE; } - @Override - protected AnnotationMirror iteratorPreserveRemove() { - return ITERATOR_PRESERVE_REMOVE; - } - @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 79c5f04187de..4f7ecd7836fa 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -51,9 +51,6 @@ public class ShrinkAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactor /** The {@code @}{@link PolyShrinkable} qualifier. */ private final AnnotationMirror POLY_SHRINKABLE; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ - private final AnnotationMirror ITERATOR_PRESERVE_REMOVE; - /** * Creates a ShrinkAnnotatedTypeFactory. * @@ -72,8 +69,6 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyShrinkable.class); - this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); postInit(); } @@ -98,11 +93,6 @@ protected AnnotationMirror polyCapability() { return POLY_SHRINKABLE; } - @Override - protected AnnotationMirror iteratorPreserveRemove() { - return ITERATOR_PRESERVE_REMOVE; - } - @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( From 6556506397d47f85a3c119de6d2da3485d89f54b Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 30 May 2026 17:52:46 -0700 Subject: [PATCH 198/308] removed isIteratorMethod and isListIteratorMethod. simplified. Co-authored-by: Copilot --- .../ModifiabilityAnnotatedTypeFactory.java | 67 ------------------- .../grow/GrowAnnotatedTypeFactory.java | 5 +- .../replace/ReplaceAnnotatedTypeFactory.java | 5 +- .../shrink/ShrinkAnnotatedTypeFactory.java | 11 ++- 4 files changed, 17 insertions(+), 71 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 9616d70b535e..1bc8bb951601 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -3,9 +3,6 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -13,17 +10,9 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** Shared annotated type factory logic for the Modifiability sub-checkers. */ public abstract class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - - /** The erased {@code java.util.ListIterator} type. */ - private final TypeMirror listIteratorErasure; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ protected final AnnotationMirror ITERATOR_PRESERVE_REMOVE; @@ -35,12 +24,6 @@ public abstract class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTyp @SuppressWarnings("this-escape") protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - - Types types = getProcessingEnv().getTypeUtils(); - this.iteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); - this.listIteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); } @@ -131,56 +114,6 @@ protected void refineIteratorReturnType( } } - /** - * Returns true if this invocation is a {@code listIterator()} method that returns a ListIterator. - * - * @param tree the method invocation to test - * @param methodType the annotated executable type of the invoked method - * @return true if this invocation returns a ListIterator from {@code listIterator()} - */ - protected boolean isListIteratorMethod( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null) { - return false; - } - // quick syntax check before expensive erasure checks - if (!invokedMethod.getSimpleName().contentEquals("listIterator") - || tree.getArguments().size() > 1) { - return false; - } - // Check if the return type is an erased ListIterator - TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types); - } - - /** - * Returns true if this invocation is an iterator-returning {@code iterator()} or {@code - * listIterator()} method. - * - * @param tree the method invocation to test - * @param methodType the annotated executable type of the invoked method - * @return true if this invocation returns an Iterator from {@code iterator()} or {@code - * listIterator()} - */ - protected boolean isIteratorMethod( - MethodInvocationTree tree, AnnotatedExecutableType methodType) { - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (invokedMethod == null) { - return false; - } - // quick syntax check before expensive erasure checks - String methodName = invokedMethod.getSimpleName().toString(); - int argCount = tree.getArguments().size(); - if (!((methodName.equals("iterator") && argCount == 0) - || (methodName.equals("listIterator") && argCount <= 1))) { - return false; - } - // Check if the return type is an erased Iterator - TypeMirror returnUnderlying = methodType.getReturnType().getUnderlyingType(); - return TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types); - } - /** Returns the top/unknown qualifier. */ protected abstract AnnotationMirror maybeCapability(); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 1af181101ce1..abcddfceafc4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -152,10 +152,13 @@ protected ParameterizedExecutableType methodFromUse( ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); AnnotatedExecutableType method = mType.executableType(); - if (isListIteratorMethod(tree, method)) { + // if the method is listIterator(). + TypeMirror returnUnderlying = method.getReturnType().getUnderlyingType(); + if (TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types)) { refineIteratorReturnType(tree, method); } + // if the method is annotated @PreservesModifiability ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { refinePreservesModifiabilityReturnType(tree, method); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index dcec1177b258..5625f7a4a307 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -168,10 +168,13 @@ protected ParameterizedExecutableType methodFromUse( ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); AnnotatedExecutableType method = mType.executableType(); - if (isListIteratorMethod(tree, method)) { + // if the method is listIterator(). + TypeMirror returnUnderlying = method.getReturnType().getUnderlyingType(); + if (TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types)) { refineIteratorReturnType(tree, method); } + // if the method is annotated @PreservesModifiability ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { refinePreservesModifiabilityReturnType(tree, method); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 4f7ecd7836fa..7c2cfd166170 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -37,6 +37,9 @@ public class ShrinkAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactor /** The erased {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryErasure; + /** The erased {@code java.util.Iterator} type. */ + private final TypeMirror iteratorErasure; + // -- Hierarchy qualifiers ---------- /** The {@code @}{@link MaybeShrinkable} qualifier. */ @@ -63,7 +66,8 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { Types types = getProcessingEnv().getTypeUtils(); this.mapEntryErasure = types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); - + this.iteratorErasure = + types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. this.MAYBE_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrinkable.class); this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); @@ -143,10 +147,13 @@ protected ParameterizedExecutableType methodFromUse( ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); AnnotatedExecutableType method = mType.executableType(); - if (isIteratorMethod(tree, method)) { + // if the method is iterator() (including listIterator()). + TypeMirror returnUnderlying = method.getReturnType().getUnderlyingType(); + if (TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types)) { refineIteratorReturnType(tree, method); } + // if the method is annotated @PreservesModifiability ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { refinePreservesModifiabilityReturnType(tree, method); From e15d571335a5c1a3b27873f89e4d2ab78397f664 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 30 May 2026 17:57:54 -0700 Subject: [PATCH 199/308] clean up typeCannotGrow and typeCannotShrink --- .../grow/GrowAnnotatedTypeFactory.java | 12 +----------- .../shrink/ShrinkAnnotatedTypeFactory.java | 14 -------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index abcddfceafc4..c5af6981fdf9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -177,18 +177,8 @@ private boolean typeCannotGrow(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { return false; } - return isMapEntry(type) + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types) || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } - - /** - * Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. - * - * @param type the type to test - * @return true if {@code type} is a subtype of {@link java.util.Map.Entry} - */ - private boolean isMapEntry(TypeMirror type) { - return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 7c2cfd166170..6843ff2df835 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -7,7 +7,6 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; @@ -169,19 +168,6 @@ protected ParameterizedExecutableType methodFromUse( * @return true if {@code type} structurally cannot support shrink operations */ private boolean typeCannotShrink(TypeMirror type) { - return isMapEntry(type); - } - - /** - * Returns true if {@code type} is a subtype of {@link java.util.Map.Entry}. - * - * @param type the type to test - * @return true if {@code type} is a subtype of {@link java.util.Map.Entry} - */ - private boolean isMapEntry(TypeMirror type) { - if (type.getKind() != TypeKind.DECLARED) { - return false; - } return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } } From 77f7b4946127726484ef067652fa561158b2cfa7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sat, 30 May 2026 23:44:19 -0700 Subject: [PATCH 200/308] fully remove isMayEntry --- .../checker/modifiability/grow/GrowAnnotatedTypeFactory.java | 4 +++- .../modifiability/shrink/ShrinkAnnotatedTypeFactory.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index c5af6981fdf9..346d53aa385e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -136,7 +136,9 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, Unmodifiable.class)) { return typeCannotGrow(tm) ? MAYBE_GROWABLE : UNGROWABLE; } else if (areSameByClass(annotation, PolyModifiable.class)) { - return isMapEntry(tm) ? MAYBE_GROWABLE : POLY_GROWABLE; + return TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) + ? MAYBE_GROWABLE + : POLY_GROWABLE; } } if (areSameByClass(annotation, MaybeModifiable.class) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 6843ff2df835..1e705bf5c53b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -130,7 +130,9 @@ public AnnotationMirror canonicalAnnotation( } else if (areSameByClass(annotation, Unmodifiable.class)) { return typeCannotShrink(tm) ? MAYBE_SHRINKABLE : UNSHRINKABLE; } else if (areSameByClass(annotation, PolyModifiable.class)) { - return isMapEntry(tm) ? MAYBE_SHRINKABLE : POLY_SHRINKABLE; + return TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) + ? MAYBE_SHRINKABLE + : POLY_SHRINKABLE; } } if (areSameByClass(annotation, MaybeModifiable.class) From d03275addbc09d94c5d232c1218a78b9349890f7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 31 May 2026 00:04:34 -0700 Subject: [PATCH 201/308] add return comments in ModifiabilityAnnotatedTypeFactory --- .../ModifiabilityAnnotatedTypeFactory.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 1bc8bb951601..6600d36784e9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -114,15 +114,31 @@ protected void refineIteratorReturnType( } } - /** Returns the top/unknown qualifier. */ + /** + * Returns the top/unknown qualifier. + * + * @return the top/unknown qualifier + */ protected abstract AnnotationMirror maybeCapability(); - /** Returns the positive capability qualifier. */ + /** + * Returns the positive capability qualifier. + * + * @return the positive capability qualifier + */ protected abstract AnnotationMirror positiveCapability(); - /** Returns the negative capability qualifier. */ + /** + * Returns the negative capability qualifier. + * + * @return the negative capability qualifier + */ protected abstract AnnotationMirror negativeCapability(); - /** Returns the polymorphic capability qualifier. */ + /** + * Returns the polymorphic capability qualifier. + * + * @return the polymorphic capability qualifier + */ protected abstract AnnotationMirror polyCapability(); } From 71304586e860ccd141326d32f5ed18caa4ebfc97 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 31 May 2026 13:10:18 -0700 Subject: [PATCH 202/308] implement override rules that any override method should preserve the modifiability of the parent method. Co-authored-by: Copilot --- .../modifiability/ModifiabilityVisitor.java | 57 +++++- .../ExactReceiverOverrideTest.java | 170 ++++++++++++++++++ .../modifiability/IteratorPrecisionTest.java | 3 + 3 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 checker/tests/modifiability/ExactReceiverOverrideTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index bc521b8c5c95..b06c82f1860c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -13,9 +13,9 @@ import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.qual.ThrowsUOE; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -32,7 +32,7 @@ * legitimately produce {@code @Modifiable}. * */ -public class ModifiabilityVisitor extends BaseTypeVisitor { +public class ModifiabilityVisitor extends BaseTypeVisitor { /** The erased {@code java.util.Collection} type. */ private final TypeMirror collectionErasure; @@ -98,6 +98,59 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { return super.visitMethodInvocation(node, p); } + /** + * Returns the positive qualifier for this checker's modifiability hierarchy, such as + * {@code @Growable}, {@code @Shrinkable}, or {@code @Replaceable}. + * + * @return this checker's positive capability qualifier + */ + private AnnotationMirror positiveCapability() { + return atypeFactory.positiveCapability(); + } + + /** + * Checks the normal override rules, then requires overrides to preserve any positive + * modifiability receiver capability from the overridden method. + * + *

For example, if the overridden method requires a {@code @Growable} receiver, then the + * overriding method must also require a {@code @Growable} receiver. + * + *

The framework's ordinary receiver override rule allows an overriding method to relax + * receiver preconditions. For modifiability operations, that would allow a subtype method to drop + * a required {@code @Growable}, {@code @Shrinkable}, or {@code @Replaceable} receiver capability. + */ + @Override + protected boolean checkOverride( + MethodTree overriderTree, + AnnotatedExecutableType overriderMethodType, + AnnotatedDeclaredType overriderType, + AnnotatedExecutableType overriddenMethodType, + AnnotatedDeclaredType overriddenType) { + if (!super.checkOverride( + overriderTree, overriderMethodType, overriderType, overriddenMethodType, overriddenType)) { + return false; + } + + AnnotatedDeclaredType overriderReceiver = overriderMethodType.getReceiverType(); + AnnotatedDeclaredType overriddenReceiver = overriddenMethodType.getReceiverType(); + AnnotationMirror positiveCapability = positiveCapability(); + + if (overriddenReceiver.hasPrimaryAnnotation(positiveCapability) + && !overriderReceiver.hasPrimaryAnnotation(positiveCapability)) { + checker.reportError( + overriderTree, + "override.receiver", + overriderReceiver, + overriddenReceiver, + overriderType, + overriderMethodType, + overriddenType, + overriddenMethodType); + return false; + } + return true; + } + /** * Returns true if this checker should report {@code @ThrowsUOE} errors. * diff --git a/checker/tests/modifiability/ExactReceiverOverrideTest.java b/checker/tests/modifiability/ExactReceiverOverrideTest.java new file mode 100644 index 000000000000..265ace54735a --- /dev/null +++ b/checker/tests/modifiability/ExactReceiverOverrideTest.java @@ -0,0 +1,170 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +@SuppressWarnings("modifiability:annotation.unverified") +public class ExactReceiverOverrideTest { + + static class GrowParent extends ArrayList { + void update(@Growable GrowParent this) {} + } + + static class GrowChildImplicit extends GrowParent { + @Override + // :: error: [override.receiver] + void update() {} + } + + static class GrowChildExact extends GrowParent { + @Override + void update(@Growable GrowChildExact this) {} + } + + static class ShrinkParent extends ArrayList { + void update(@Shrinkable ShrinkParent this) {} + } + + static class ShrinkChildImplicit extends ShrinkParent { + @Override + // :: error: [override.receiver] + void update() {} + } + + static class ShrinkChildExact extends ShrinkParent { + @Override + void update(@Shrinkable ShrinkChildExact this) {} + } + + static class ReplaceParent extends ArrayList { + void update(@Replaceable ReplaceParent this) {} + } + + static class ReplaceChildImplicit extends ReplaceParent { + @Override + // :: error: [override.receiver] + void update() {} + } + + static class ReplaceChildExact extends ReplaceParent { + @Override + void update(@Replaceable ReplaceChildExact this) {} + } + + static class ModifiableParent extends ArrayList { + void update(@Modifiable ModifiableParent this) {} + } + + static class ModifiableChildImplicit extends ModifiableParent { + @Override + // :: error: [override.receiver] :: error: [override.receiver] :: error: [override.receiver] + void update() {} + } + + static class ModifiableChildExact extends ModifiableParent { + @Override + void update(@Modifiable ModifiableChildExact this) {} + } + + static class GrowShrinkParent extends ArrayList { + void update(@Growable @Shrinkable GrowShrinkParent this) {} + } + + static class GrowOnlyChild extends GrowShrinkParent { + @Override + // :: error: [override.receiver] + void update(@Growable GrowOnlyChild this) {} + } + + static class ShrinkOnlyChild extends GrowShrinkParent { + @Override + // :: error: [override.receiver] + void update(@Shrinkable ShrinkOnlyChild this) {} + } + + static class UnmodifiableParent extends ArrayList { + void update(@Unmodifiable UnmodifiableParent this) {} + } + + static class UnmodifiableChildImplicit extends UnmodifiableParent { + @Override + void update() {} + } + + static class UnmodifiableArrayList extends ArrayList { + @Override + public boolean add(@Growable UnmodifiableArrayList this, E element) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(@Growable UnmodifiableArrayList this, int index, E element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll( + @Growable UnmodifiableArrayList this, Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll( + @Growable UnmodifiableArrayList this, int index, Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public E remove(@Shrinkable UnmodifiableArrayList this, int index) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(@Shrinkable UnmodifiableArrayList this, Object element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(@Shrinkable UnmodifiableArrayList this, Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(@Shrinkable UnmodifiableArrayList this, Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf( + @Shrinkable UnmodifiableArrayList this, Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear(@Shrinkable UnmodifiableArrayList this) { + throw new UnsupportedOperationException(); + } + + @Override + // :: error: [override.receiver] + public E set(UnmodifiableArrayList this, int index, E element) { + throw new UnsupportedOperationException(); + } + + @Override + public void replaceAll(@Replaceable UnmodifiableArrayList this, UnaryOperator operator) { + throw new UnsupportedOperationException(); + } + + @Override + public void sort(@Replaceable UnmodifiableArrayList this, Comparator comparator) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index eab62642ce4e..379c88f9f772 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -408,16 +408,19 @@ public int previousIndex() { } @Override + // :: error: [override.receiver] public void remove() { throw new UnsupportedOperationException(); } @Override + // :: error: [override.receiver] public void set(String e) { throw new UnsupportedOperationException(); } @Override + // :: error: [override.receiver] public void add(String e) { throw new UnsupportedOperationException(); } From b51f25466554df523de22c86ec48b9128c644d09 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 31 May 2026 13:48:10 -0700 Subject: [PATCH 203/308] fixed nullness checker error in CI. --- .../ModifiabilityAnnotatedTypeFactory.java | 3 +-- .../grow/GrowAnnotatedTypeFactory.java | 18 ++++++--------- .../replace/ReplaceAnnotatedTypeFactory.java | 23 ++++++++----------- .../shrink/ShrinkAnnotatedTypeFactory.java | 16 +++++-------- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 6600d36784e9..c4b0242ae87a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -24,8 +24,7 @@ public abstract class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTyp @SuppressWarnings("this-escape") protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - this.ITERATOR_PRESERVE_REMOVE = - AnnotationBuilder.fromClass(getElementUtils(), IteratorPolyMod.class); + this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 346d53aa385e..8d5c33df490c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -9,7 +9,6 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; @@ -66,18 +65,15 @@ public class GrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - Types types = getProcessingEnv().getTypeUtils(); - this.mapEntryErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); - this.iteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + this.mapEntryErasure = types.erasure(elements.getTypeElement("java.util.Map.Entry").asType()); + this.iteratorErasure = types.erasure(elements.getTypeElement("java.util.Iterator").asType()); this.listIteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); + types.erasure(elements.getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeGrowable.class); - this.GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Growable.class); - this.UNGROWABLE = AnnotationBuilder.fromClass(getElementUtils(), Ungrowable.class); - this.POLY_GROWABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyGrowable.class); + this.MAYBE_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeGrowable.class); + this.GROWABLE = AnnotationBuilder.fromClass(elements, Growable.class); + this.UNGROWABLE = AnnotationBuilder.fromClass(elements, Ungrowable.class); + this.POLY_GROWABLE = AnnotationBuilder.fromClass(elements, PolyGrowable.class); postInit(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 5625f7a4a307..60dab6cf2b8a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -9,7 +9,6 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; @@ -76,23 +75,21 @@ public class ReplaceAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFacto public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - Types types = getProcessingEnv().getTypeUtils(); - this.setErasure = types.erasure(getElementUtils().getTypeElement("java.util.Set").asType()); + this.setErasure = types.erasure(elements.getTypeElement("java.util.Set").asType()); this.collectionErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Collection").asType()); - this.queueErasure = types.erasure(getElementUtils().getTypeElement("java.util.Queue").asType()); + types.erasure(elements.getTypeElement("java.util.Collection").asType()); + this.queueErasure = types.erasure(elements.getTypeElement("java.util.Queue").asType()); this.linkedListErasure = - types.erasure(getElementUtils().getTypeElement("java.util.LinkedList").asType()); - this.iteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + types.erasure(elements.getTypeElement("java.util.LinkedList").asType()); + this.iteratorErasure = types.erasure(elements.getTypeElement("java.util.Iterator").asType()); this.listIteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.ListIterator").asType()); + types.erasure(elements.getTypeElement("java.util.ListIterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeReplaceable.class); - this.REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Replaceable.class); - this.UNREPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), Unreplaceable.class); - this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyReplaceable.class); + this.MAYBE_REPLACEABLE = AnnotationBuilder.fromClass(elements, MaybeReplaceable.class); + this.REPLACEABLE = AnnotationBuilder.fromClass(elements, Replaceable.class); + this.UNREPLACEABLE = AnnotationBuilder.fromClass(elements, Unreplaceable.class); + this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(elements, PolyReplaceable.class); postInit(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 1e705bf5c53b..1c9e05ffb437 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -8,7 +8,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Types; import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; @@ -62,16 +61,13 @@ public class ShrinkAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactor public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - Types types = getProcessingEnv().getTypeUtils(); - this.mapEntryErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Map.Entry").asType()); - this.iteratorErasure = - types.erasure(getElementUtils().getTypeElement("java.util.Iterator").asType()); + this.mapEntryErasure = types.erasure(elements.getTypeElement("java.util.Map.Entry").asType()); + this.iteratorErasure = types.erasure(elements.getTypeElement("java.util.Iterator").asType()); // Initialize annotation mirrors after the hierarchy is established. - this.MAYBE_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), MaybeShrinkable.class); - this.SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Shrinkable.class); - this.UNSHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), Unshrinkable.class); - this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(getElementUtils(), PolyShrinkable.class); + this.MAYBE_SHRINKABLE = AnnotationBuilder.fromClass(elements, MaybeShrinkable.class); + this.SHRINKABLE = AnnotationBuilder.fromClass(elements, Shrinkable.class); + this.UNSHRINKABLE = AnnotationBuilder.fromClass(elements, Unshrinkable.class); + this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(elements, PolyShrinkable.class); postInit(); } From 49303b9b587f6e309bac496f5f3fad2428fc8667 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 31 May 2026 14:20:21 -0700 Subject: [PATCH 204/308] fixed nullable warning. --- .../checker/modifiability/ModifiabilityVisitor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index b06c82f1860c..f8d94a99572b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -133,6 +133,10 @@ protected boolean checkOverride( AnnotatedDeclaredType overriderReceiver = overriderMethodType.getReceiverType(); AnnotatedDeclaredType overriddenReceiver = overriddenMethodType.getReceiverType(); + if (overriderReceiver == null || overriddenReceiver == null) { + return true; + } + AnnotationMirror positiveCapability = positiveCapability(); if (overriddenReceiver.hasPrimaryAnnotation(positiveCapability) From 17117174bff489be70be48b927bb50824604b212 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Jun 2026 14:17:50 -0700 Subject: [PATCH 205/308] add Seqgrow for sequenced collection, does not include sequenced map yet. --- .../modifiability/qual/BottomSeqGrowable.java | 21 +++ .../modifiability/qual/MaybeSeqGrowable.java | 26 ++++ .../modifiability/qual/Modifiable.java | 8 +- .../modifiability/qual/PolyModifiable.java | 5 +- .../modifiability/qual/PolySeqGrowable.java | 20 +++ .../modifiability/qual/SeqGrowable.java | 23 +++ .../modifiability/qual/SeqUngrowable.java | 23 +++ .../modifiability/ModifiabilityChecker.java | 8 +- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 145 ++++++++++++++++++ .../modifiability/seqgrow/SeqGrowChecker.java | 26 ++++ .../modifiability/seqgrow/SeqGrowVisitor.java | 23 +++ .../CollectionsModifiableTest.java | 7 +- .../tests/modifiability/SeqGrowableTest.java | 88 +++++++++++ .../tests/modifiability/ThrowsUOETest.java | 8 +- docs/manual/modifiability-checker.tex | 133 +++++++++------- 15 files changed, 493 insertions(+), 71 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomSeqGrowable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolySeqGrowable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java create mode 100644 checker/tests/modifiability/SeqGrowableTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomSeqGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomSeqGrowable.java new file mode 100644 index 000000000000..4c7f8a15713f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomSeqGrowable.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.modifiability.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 bottom qualifier in the sequenced-grow hierarchy. + * + *

Programmers should rarely write this qualifier. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({SeqGrowable.class, SeqUngrowable.class}) +public @interface BottomSeqGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java new file mode 100644 index 000000000000..edc800772346 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.modifiability.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 top qualifier in the sequenced-grow hierarchy. The collection may or may not support + * positional grow operations such as {@code addFirst} and {@code addLast}. The checker + * conservatively issues an error wherever a sequenced-grow operation is called on a + * {@code @MaybeSeqGrowable} expression. + * + *

This is the default qualifier for unannotated types in the sequenced-grow hierarchy. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface MaybeSeqGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java index 6ea06c0065be..978239c8b045 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.java @@ -7,13 +7,13 @@ import java.lang.annotation.Target; /** - * Convenience alias usually meaning {@code @Growable @Shrinkable @Replaceable}. Calling a mutating - * operation (growing, shrinking, or replacing) on this collection will not throw {@link - * UnsupportedOperationException}. + * Convenience alias usually meaning {@code @Growable @Shrinkable @Replaceable @SeqGrowable}. + * Calling a mutating operation (growing, shrinking, replacing, or sequenced-growing) on this + * collection will not throw {@link UnsupportedOperationException}. * *

As an exception, {@code @Modifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Modifiable Iterator} means - * {@code @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator}. + * {@code @MaybeGrowable @Shrinkable @MaybeReplaceable @MaybeSeqGrowable Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 5176b1123f9d..83724f7d4be5 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -4,8 +4,9 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @PolyGrowable @PolyReplaceable @PolyShrinkable}. A polymorphic - * qualifier for all three modifiability hierarchies. + * Convenience alias meaning + * {@code @PolyGrowable @PolyReplaceable @PolyShrinkable @PolySeqGrowable}. A polymorphic qualifier + * for all four modifiability hierarchies. * *

Write {@code @PolyModifiable} on methods that preserve or transfer modifiability, such as * {@code List.subList()} and {@code stream()}. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolySeqGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolySeqGrowable.java new file mode 100644 index 000000000000..5edd895d7900 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolySeqGrowable.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.modifiability.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 sequenced-grow hierarchy. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(MaybeSeqGrowable.class) +public @interface PolySeqGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java new file mode 100644 index 000000000000..ce329fe46e61 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling sequenced-grow operations such as {@code addFirst}, {@code addLast}, {@code offerFirst}, + * {@code offerLast}, and {@code push} on this collection will not throw {@link + * UnsupportedOperationException}. + * + *

No guarantees are made about ordinary grow, shrink, or replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(MaybeSeqGrowable.class) +public @interface SeqGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java new file mode 100644 index 000000000000..26c67f1cfeb8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Calling sequenced-grow operations such as {@code addFirst}, {@code addLast}, {@code offerFirst}, + * {@code offerLast}, and {@code push} on this collection will throw {@link + * UnsupportedOperationException}. + * + *

No guarantees are made about ordinary grow, shrink, or replace operations. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(MaybeSeqGrowable.class) +public @interface SeqUngrowable {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 4ae41a814153..6367976116e2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -4,6 +4,7 @@ import java.util.List; import org.checkerframework.checker.modifiability.grow.GrowChecker; import org.checkerframework.checker.modifiability.replace.ReplaceChecker; +import org.checkerframework.checker.modifiability.seqgrow.SeqGrowChecker; import org.checkerframework.checker.modifiability.shrink.ShrinkChecker; import org.checkerframework.framework.source.AggregateChecker; import org.checkerframework.framework.source.SourceChecker; @@ -12,11 +13,13 @@ * A type-checker that warns, at compile time, if a program might throw {@link * UnsupportedOperationException} at run time due to calling a mutating method on a collection. * - *

This is an aggregate checker that runs three independent sub-checkers, one for each + *

This is an aggregate checker that runs four independent sub-checkers, one for each * modifiability capability: * *

    *
  • {@link GrowChecker} checks grow operations (e.g., {@code add}, {@code addAll}) + *
  • {@link SeqGrowChecker} checks sequenced grow operations (e.g., {@code addFirst}, {@code + * addLast}) *
  • {@link ShrinkChecker} checks shrink operations (e.g., {@code remove}, {@code clear}) *
  • {@link ReplaceChecker} checks replace operations (e.g., {@code set}, {@code replaceAll}) *
@@ -30,6 +33,7 @@ public ModifiabilityChecker() {} @Override protected Collection> getSupportedCheckers() { - return List.of(GrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); + return List.of( + GrowChecker.class, SeqGrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java new file mode 100644 index 000000000000..2d4f6fc0f148 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -0,0 +1,145 @@ +package org.checkerframework.checker.modifiability.seqgrow; + +import com.sun.source.tree.MethodInvocationTree; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.qual.BottomSeqGrowable; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolySeqGrowable; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +/** The annotated type factory for the {@link SeqGrowChecker}. */ +public class SeqGrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { + + /** The erased {@code java.util.SequencedCollection} type. */ + private final TypeMirror sequencedCollectionErasure; + + /** The {@code @}{@link MaybeSeqGrowable} qualifier. */ + private final AnnotationMirror MAYBE_SEQ_GROWABLE; + + /** The {@code @}{@link SeqGrowable} qualifier. */ + private final AnnotationMirror SEQ_GROWABLE; + + /** The {@code @}{@link SeqUngrowable} qualifier. */ + private final AnnotationMirror SEQ_UNGROWABLE; + + /** The {@code @}{@link PolySeqGrowable} qualifier. */ + private final AnnotationMirror POLY_SEQ_GROWABLE; + + /** + * Creates a SeqGrowAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + + this.sequencedCollectionErasure = + types.erasure(elements.getTypeElement("java.util.SequencedCollection").asType()); + this.MAYBE_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeSeqGrowable.class); + this.SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, SeqGrowable.class); + this.SEQ_UNGROWABLE = AnnotationBuilder.fromClass(elements, SeqUngrowable.class); + this.POLY_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, PolySeqGrowable.class); + + postInit(); + } + + @Override + protected AnnotationMirror maybeCapability() { + return MAYBE_SEQ_GROWABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return SEQ_GROWABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return SEQ_UNGROWABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_SEQ_GROWABLE; + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + MaybeSeqGrowable.class, + SeqGrowable.class, + SeqUngrowable.class, + BottomSeqGrowable.class, + PolySeqGrowable.class)); + } + + @Override + public AnnotationMirror canonicalAnnotation( + AnnotationMirror annotation, @Nullable TypeMirror tm) { + if (tm != null) { + if (areSameByClass(annotation, Modifiable.class)) { + return typeCannotSeqGrow(tm) ? MAYBE_SEQ_GROWABLE : SEQ_GROWABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return typeCannotSeqGrow(tm) ? MAYBE_SEQ_GROWABLE : SEQ_UNGROWABLE; + } + } + if (areSameByClass(annotation, MaybeModifiable.class) + || areSameByClass(annotation, UnmodifiableParam.class)) { + return MAYBE_SEQ_GROWABLE; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return POLY_SEQ_GROWABLE; + } + return super.canonicalAnnotation(annotation); + } + + @Override + protected ParameterizedExecutableType methodFromUse( + MethodInvocationTree tree, boolean inferTypeArgs) { + ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + AnnotatedExecutableType method = mType.executableType(); + + // if the method is annotated @PreservesModifiability + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refinePreservesModifiabilityReturnType(tree, method); + } + + return mType; + } + + /** + * Returns true if {@code type} structurally cannot support sequenced grow operations. + * + *

Only sequenced collections can support sequenced grow operations. + * + * @param type the type to test + * @return true if {@code type} structurally cannot support sequenced grow operations + */ + private boolean typeCannotSeqGrow(TypeMirror type) { + return type.getKind() != TypeKind.DECLARED + || !TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java new file mode 100644 index 000000000000..4d9f57ab3093 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.modifiability.seqgrow; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * A type-checker that warns, at compile time, if a program might throw {@link + * UnsupportedOperationException} at run time due to calling a sequenced-grow method on a + * collection. + * + *

The checker enforces the Modifiability type system, where {@code @SeqGrowable} collections can + * be safely added to at the front or back, {@code @SeqUngrowable} collections cannot, and + * {@code @MaybeSeqGrowable} collections have unknown sequenced-grow behavior. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) +@SuppressWarningsPrefix({"seqgrowable", "modifiability"}) +public class SeqGrowChecker extends ModifiabilityBaseChecker { + /** Creates a SeqGrowChecker. */ + public SeqGrowChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java new file mode 100644 index 000000000000..7e5e981b1730 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.modifiability.seqgrow; + +import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** Visitor for the {@link SeqGrowChecker}. */ +public class SeqGrowVisitor extends ModifiabilityVisitor { + + /** + * Creates a visitor for the SeqGrow Checker. + * + * @param checker the SeqGrow Checker + */ + public SeqGrowVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + protected boolean shouldCheckThrowsUOE() { + // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. + return false; + } +} diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index 6a0b56b86c96..70abc93f2640 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -17,7 +17,11 @@ import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; public class CollectionsModifiableTest { @@ -285,7 +289,8 @@ void testUnmodifiableWrappers() { // :: error: [assignment] @Modifiable Map modM = Collections.unmodifiableMap(m); - @Modifiable SortedSet ss = new TreeSet<>(); + @Growable @Shrinkable @Replaceable @SeqUngrowable + SortedSet ss = new TreeSet<>(); @Unmodifiable SortedSet unmodSS = Collections.unmodifiableSortedSet(ss); // :: error: [method.invocation] unmodSS.add("a"); diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java new file mode 100644 index 000000000000..c724c4b8054d --- /dev/null +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -0,0 +1,88 @@ +// @below-java21-jdk-skip-test + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.SequencedCollection; +import java.util.TreeSet; +import java.util.Vector; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; + +public class SeqGrowableTest { + + void constructorPositiveCases() { + @SeqGrowable ArrayDeque arrayDeque = new ArrayDeque<>(); + @SeqGrowable LinkedList linkedList = new LinkedList<>(); + @SeqGrowable ArrayList arrayList = new ArrayList<>(); + @SeqGrowable Vector vector = new Vector<>(); + @SeqGrowable LinkedHashSet linkedHashSet = new LinkedHashSet<>(); + @SeqGrowable CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList<>(); + @SeqGrowable + ConcurrentLinkedDeque concurrentLinkedDeque = new ConcurrentLinkedDeque<>(); + @SeqGrowable LinkedBlockingDeque linkedBlockingDeque = new LinkedBlockingDeque<>(); + + arrayDeque.addFirst("a"); + linkedList.addLast("b"); + arrayList.addFirst("c"); + vector.addLast("d"); + linkedHashSet.addFirst("e"); + copyOnWriteArrayList.addLast("f"); + concurrentLinkedDeque.offerFirst("g"); + linkedBlockingDeque.offerLast("h"); + } + + void dequeOperations(@SeqGrowable Deque deque) { + deque.addFirst("a"); + deque.addLast("b"); + deque.offerFirst("c"); + deque.offerLast("d"); + deque.push("e"); + } + + void ordinaryGrowDoesNotImplySeqGrow(@Growable Deque growOnly) { + growOnly.add("a"); + // :: error: [method.invocation] + growOnly.addFirst("b"); + } + + void maybeSeqGrowableIsRejected( + SequencedCollection unknown, + @MaybeSeqGrowable List maybeList, + @MaybeSeqGrowable Deque maybeDeque) { + // :: error: [method.invocation] + unknown.addFirst("a"); + + // :: error: [method.invocation] + maybeList.addLast("b"); + + // :: error: [method.invocation] + maybeDeque.offerFirst("c"); + } + + void sortedSetsAreSeqUngrowable() { + @Growable TreeSet growableTreeSet = new TreeSet<>(); + growableTreeSet.add("a"); + + @SeqUngrowable TreeSet seqUngrowableTreeSet = new TreeSet<>(); + @SeqUngrowable + ConcurrentSkipListSet seqUngrowableConcurrentSet = new ConcurrentSkipListSet<>(); + + // :: error: [assignment] + @SeqGrowable TreeSet seqGrowableTreeSet = new TreeSet<>(); + + @SeqGrowable + ConcurrentSkipListSet seqGrowableConcurrentSet = + // :: error: [assignment] + new ConcurrentSkipListSet<>(); + } +} diff --git a/checker/tests/modifiability/ThrowsUOETest.java b/checker/tests/modifiability/ThrowsUOETest.java index 78869ba5a657..f3fa5e2a029f 100644 --- a/checker/tests/modifiability/ThrowsUOETest.java +++ b/checker/tests/modifiability/ThrowsUOETest.java @@ -11,10 +11,10 @@ public class ThrowsUOETest { void testSortedSet(SortedSet s) { - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] s.addFirst("foo"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] s.addLast("bar"); } @@ -23,10 +23,10 @@ void testImplementation(TreeSet ts) { // We verify that calling them on a concrete implementation also triggers the // warning. - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] ts.addFirst("foo"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] ts.addLast("bar"); } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index d8868bb601ab..4d1ee2a980ca 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -66,15 +66,14 @@ called on any collection, whether modifiable or unmodifiable. Some collections may be modified in some ways but not in others. -A collection can be modified in three different ways: -\textbf{Grow}, \textbf{Shrink}, and \textbf{Replace}. +A collection can be modified in four different ways: +\textbf{Grow}, \textbf{SeqGrow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements (\, \, \, \ldots). - \item \textbf{GrowSequenced}: The collection can grow by adding new - elements at the beginning or end (\, \). - Because GrowSequenced checking is exactly like Grow checking for a - different set of methods, this chapter often omits it for brevity. + \item \textbf{SeqGrow}: A sequenced collection can grow by adding new + elements at the beginning or end (\, \, + \, \, \, \ldots). \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing @@ -95,24 +94,23 @@ 2-element hierarchy to express whether \ can be called on a collection's iterator. -% TODO: add a figure that follows the sturucture of other checker manuals. +% TODO: add a figure that follows the structure of other checker manuals. \begin{figure} \begin{smaller} \centering \begin{minipage}{0.85\linewidth} \begin{verbatim} - Grow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy + Grow hierarchy SeqGrow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy - @MaybeGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyMod - / \ / \ / \ | - @Growable @Ungrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | - \ / \ / \ / | - @BottomGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyMod + @MaybeGrowable @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable @MaybeIteratorPolyMod + / \ / \ / \ / \ | + @Growable @Ungrowable @SeqGrowable @SeqUngrowable @Shrinkable @Unshrinkable @Replaceable @Unreplaceable | + \ / \ / \ / \ / | + @BottomGrowable @BottomSeqGrowable @BottomShrinkable @BottomReplaceable @IteratorPolyMod \end{verbatim} \end{minipage} \end{smaller} \caption{Independent modifiability qualifier hierarchies. - (The GrowSequenced hierarchy is omitted for brevity.) Each annotated type carries one qualifier from each hierarchy.} \label{fig-modifiability-lattice} \end{figure} @@ -121,39 +119,39 @@ Each hierarchy is type-checked individually. An assignment \code{T x = y} is valid only if the compile-time type of \code{y} is a subtype of (or equal to) \code{T} in every hierarchy simultaneously. For -example, \code{\emph{@Growable} @MaybeShrinkable @MaybeReplaceable List} is a -subtype of \code{\emph{@MaybeGrowable} @MaybeShrinkable @MaybeReplaceable List} +example, \code{\emph{@Growable} @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable List} is a +subtype of \code{\emph{@MaybeGrowable} @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable List} (because \<@Growable> $\le$ \<@MaybeGrowable>), but it is \emph{not} a -subtype of \code{\emph{@Ungrowable} @MaybeShrinkable @MaybeReplaceable List} +subtype of \code{\emph{@Ungrowable} @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable List} (because \<@Growable> and \<@Ungrowable> are incomparable siblings in the Grow hierarchy). \subsectionAndLabel{Alias annotations}{modifiability-aliases} -For convenience, the Modifiability Checker provides four alias +For convenience, the Modifiability Checker provides five alias annotations. They are not part of any hierarchy; the checker expands each one into its constituent qualifiers when it appears in source code. \begin{description} \item[\refqualclass{checker/modifiability/qual}{Modifiable}] -Alias for \code{@Growable @Shrinkable @Replaceable} (but see +Alias for \code{@Growable @SeqGrowable @Shrinkable @Replaceable} (but see Section~\ref{modifiability-types-lacking-methods} for exceptions). Calling any mutating operation on this collection will not throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{Unmodifiable}] -Alias for \code{@Ungrowable @Unshrinkable @Unreplaceable} (but see +Alias for \code{@Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable} (but see Section~\ref{modifiability-types-lacking-methods} for exceptions). The collection is definitely unmodifiable: calling any mutating operation always throws \UnsupportedOperationException. The checker issues a warning -at every invocation of a grow, shrink, or replace operation. +at every invocation of a grow, seq-grow, shrink, or replace operation. \item[\refqualclass{checker/modifiability/qual}{MaybeModifiable}] -Alias for \code{@MaybeGrowable @MaybeShrinkable @MaybeReplaceable}. +Alias for \code{@MaybeGrowable @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable}. The checker cannot determine the modifiability of the collection. The checker conservatively issues a warning at every invocation of a grow, -shrink, or replace operation. +seq-grow, shrink, or replace operation. \item[\refqualclass{checker/modifiability/qual}{UnmodifiableParam}] Syntactic sugar for \refqualclass{checker/modifiability/qual}{MaybeModifiable}. @@ -174,7 +172,7 @@ unmodifiability: no alias may modify the value. \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] -Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable} +Alias for \code{@PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable} (see Section~\ref{modifiability-polymorphic}). \end{description} @@ -183,7 +181,7 @@ Examples using alias annotations: \begin{Verbatim} - @Modifiable List mod = new ArrayList<>(); // all three capabilities + @Modifiable List mod = new ArrayList<>(); // all four capabilities @Unmodifiable List unmod = List.of("a"); // no capabilities @MaybeModifiable List unknown = ...; // capabilities are unknown at compile time void foo(@UnmodifiableParam List list) { ... } // foo does not modify list @@ -215,28 +213,31 @@ The bottom qualifier in the Grow hierarchy. Programmers should rarely write it. \end{description} -\subsubsectionAndLabel{The GrowSequenced hierarchy}{modifiability-grow-hierarchy} +\subsubsectionAndLabel{The SeqGrow hierarchy}{modifiability-seqgrow-hierarchy} \begin{description} -\item[\refqualclass{checker/modifiability/qual}{MaybeGrowableSequenced}] -The top qualifier in the GrowSequenced hierarchy. -The checker cannot determine whether this collection supports grow +\item[\refqualclass{checker/modifiability/qual}{MaybeSeqGrowable}] +The top qualifier in the SeqGrow hierarchy. +The checker cannot determine whether this collection supports sequenced grow operations. Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this collection \emph{may} throw \UnsupportedOperationException. This is the default qualifier for unannotated types in the -GrowSequenced hierarchy. +SeqGrow hierarchy. -\item[\refqualclass{checker/modifiability/qual}{GrowableSequenced}] +\item[\refqualclass{checker/modifiability/qual}{SeqGrowable}] Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this collection \emph{will not} throw \UnsupportedOperationException. -\item[\refqualclass{checker/modifiability/qual}{UngrowableSequenced}] +\item[\refqualclass{checker/modifiability/qual}{SeqUngrowable}] Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this collection \emph{will} throw \UnsupportedOperationException. +For example, \ and \ support ordinary +\ but throw \UnsupportedOperationException\ for explicit positional +insertion with \ and \. -\item[\refqualclass{checker/modifiability/qual}{BottomGrowableSequenced}] -The bottom qualifier in the GrowSequenced hierarchy. Programmers should rarely write it. +\item[\refqualclass{checker/modifiability/qual}{BottomSeqGrowable}] +The bottom qualifier in the SeqGrow hierarchy. Programmers should rarely write it. \end{description} \subsubsectionAndLabel{The Shrink hierarchy}{modifiability-shrink-hierarchy} @@ -330,10 +331,15 @@ The return type's replaceability matches the replaceability of whichever formal parameter is annotated with \<@PolyReplaceable>. +\item[\refqualclass{checker/modifiability/qual}{PolySeqGrowable}] +Polymorphic qualifier for the SeqGrow hierarchy. +The return type's seq-growability matches the seq-growability of +whichever formal parameter is annotated with \<@PolySeqGrowable>. + \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] -Alias for \code{@PolyGrowable @PolyShrinkable @PolyReplaceable}. -Preserves all three capabilities simultaneously. -Use on methods such as \code{Collections.synchronizedList} thatx +Alias for \code{@PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable}. +Preserves all four capabilities simultaneously. +Use on methods such as \code{Collections.synchronizedList} that do not change modifiability. \end{description} @@ -357,10 +363,10 @@ When written on a one-argument method declaration, it indicates that the method preserves positive modifiability capabilities from its argument to its return value. If the argument is \<@Growable>, \<@Shrinkable>, -\<@Replaceable>, or \<@IteratorPolyMod>, then the Modifiability Checker +\<@Replaceable>, \<@SeqGrowable>, or \<@IteratorPolyMod>, then the Modifiability Checker treats the return value as having that same capability. If the argument has any other qualifier, the return type is the top qualifier (\<@MaybeGrowable>, -\<@MaybeShrinkable>, \<@MaybeReplaceable>, or \<@MaybeIteratorPolyMod>) in +\<@MaybeSeqGrowable>, \<@MaybeShrinkable>, \<@MaybeReplaceable>, or \<@MaybeIteratorPolyMod>) in the corresponding hierarchy. This is useful for methods that may return their argument, or may return a @@ -369,27 +375,29 @@ If put on a method with no parameters, then it has no effect. \end{description} -\sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} +\sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, seq-grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} %% TODO add explicit examples TODO, also in the Javadoc TODO. Ordinarily, the declaration \<@Modifiable MyClass x;> is equivalent to -\<@Growable @Shrinkable @Replaceable MyClass x;>, +\<@Growable @SeqGrowable @Shrinkable @Replaceable MyClass x;>, and -\<@Unodifiable MyClass x;> is equivalent to -\<@Ungrowable @Unshrinkable @Unreplaceable MyClass x;>. However, there are +\<@Unmodifiable MyClass x;> is equivalent to +\<@Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable MyClass x;>. However, there are exceptions: %% TODO add more examples, exhaustively if possible. -\begin{tabular}{rlcrrrl} -% \<@Modifiable X> & = & \<@Growable> & \<@Shrinkable> & \<@Replaceable> & \ \\ -\<@Modifiable> & \ & = & \<@Growable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ -\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ -\<@Modifiable> & \ & = & \<@Growable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ -\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ -\<@Modifiable> & \ & = & \<@MaybeGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ -\<@Unmodifiable> & \ & = & \<@MaybeGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\begin{tabular}{rlcrrrrl} +% \<@Modifiable X> & = & \<@Growable> & \<@SeqGrowable> & \<@Shrinkable> & \<@Replaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@SeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@SeqUngrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ \end{tabular} When a programmer writes \<@Modifiable> or \<@Unmodifiable> on a type that @@ -397,6 +405,11 @@ qualifier. For example, \sunjavadoc{java.base/java/util/Queue.html}{\code{Queue}} has \ and \ methods but no replacement methods. +It is not a subtype of \, so it also has no +\ or \ methods. By contrast, \ is a +\, so its seq-grow capability is relevant; the JDK +annotations mark it \<@SeqUngrowable> because its \ and +\ methods always throw \UnsupportedOperationException. Although no object of class \ supports replace operations, @@ -456,15 +469,17 @@ class Demo { void basicUsage() { - // @Modifiable is an alias for @Growable @Shrinkable @Replaceable + // @Modifiable is an alias for @Growable @SeqGrowable @Shrinkable @Replaceable @Modifiable List mod = new ArrayList<>(); mod.add("a"); // OK: @Growable guarantees add() works + mod.addFirst("z"); // OK: @SeqGrowable guarantees addFirst() works mod.remove("a"); // OK: @Shrinkable guarantees remove() works mod.set(0, "b"); // OK: @Replaceable guarantees set() works - // @Unmodifiable is an alias for @Ungrowable @Unshrinkable @Unreplaceable + // @Unmodifiable is an alias for @Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable @Unmodifiable List unmod = List.of("a", "b"); unmod.add("c"); // Error: add() requires @Growable, got @Ungrowable + unmod.addFirst("d"); // Error: addFirst() requires @SeqGrowable, got @SeqUngrowable unmod.get(0); // OK: read-only access needs no capability } @@ -493,7 +508,7 @@ map.remove("key"); // Error: @MaybeShrinkable (default) !<: @Shrinkable } - // @PolyModifiable preserves all three capabilities + // @PolyModifiable preserves all four capabilities @PolyModifiable List wrap(@PolyModifiable List list) { return list; } @@ -502,7 +517,7 @@ @Modifiable List m = wrap(mod); // OK @Growable @Shrinkable List gs2 = wrap(gs); // OK // :: error: [assignment] - @Modifiable List bad = wrap(gs); // Error: gs has @MaybeReplaceable + @Modifiable List bad = wrap(gs); // Error: gs has @MaybeSeqGrowable and @MaybeReplaceable } } \end{Verbatim} @@ -511,9 +526,11 @@ % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap % LocalWords: toArray addAll removeAll retainAll replaceAll setValue % LocalWords: MaybeModifiable UnmodifiableParam Growable MaybeGrowable MaybeShrinkable -% LocalWords: MaybeReplaceable growable ListIterator PolyModifiable addFirst -% LocalWords: synchronizedList PolyShrinkable PolyGrowable PolyReplaceable ThrowsUOE -% LocalWords: addLast unreplaceable Growability shrinkability replacability +% LocalWords: MaybeReplaceable MaybeSeqGrowable growable seq ListIterator +% LocalWords: PolyModifiable PolySeqGrowable addFirst synchronizedList +% LocalWords: PolyShrinkable PolyGrowable PolyReplaceable ThrowsUOE SeqGrow +% LocalWords: addLast offerFirst offerLast unreplaceable seq-growability +% LocalWords: SeqGrowable SeqUngrowable BottomSeqGrowable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable % LocalWords: MaybeIteratorPolyMod IteratorPolyMod PreservesModifiability From 2b2d744fa75da25372f029ab488a546a4ae6c733 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Jun 2026 16:06:33 -0700 Subject: [PATCH 206/308] ask SeqGrowVisitor to skip checkUnmodParamLocation and CheckCustomModifiabilityAnnotation --- .../modifiability/seqgrow/SeqGrowVisitor.java | 13 +++++++++++++ checker/tests/modifiability/SeqGrowableTest.java | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index 7e5e981b1730..f78decc266f8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -20,4 +20,17 @@ protected boolean shouldCheckThrowsUOE() { // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. return false; } + + @Override + protected boolean shouldCheckUnmodifiableParamLocation() { + // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location + // reporting. + return checker.getParentChecker() == null; + } + + @Override + protected boolean shouldCheckCustomModifiabilityAnnotation() { + // When running under ModifiabilityChecker, GrowChecker handles custom type warnings. + return checker.getParentChecker() == null; + } } diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java index c724c4b8054d..f333fa87d0e1 100644 --- a/checker/tests/modifiability/SeqGrowableTest.java +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -7,6 +7,7 @@ import java.util.LinkedList; import java.util.List; import java.util.SequencedCollection; +import java.util.SortedSet; import java.util.TreeSet; import java.util.Vector; import java.util.concurrent.ConcurrentLinkedDeque; @@ -20,6 +21,10 @@ public class SeqGrowableTest { + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] :: + // error:declaration.inconsistent.with.implements.clause + abstract static @SeqGrowable class UserDefinedSortedSet implements SortedSet {} + void constructorPositiveCases() { @SeqGrowable ArrayDeque arrayDeque = new ArrayDeque<>(); @SeqGrowable LinkedList linkedList = new LinkedList<>(); From 741d957a5151e8fe84ca9b40db147761b7628bf0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Jun 2026 16:12:17 -0700 Subject: [PATCH 207/308] fixed seqGrowAnnotatedTypeFactory to handle jdk under21 where sequenceCollection is not avaliable. --- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 2d4f6fc0f148..ad37c595fe4d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -7,6 +7,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; @@ -32,7 +33,7 @@ public class SeqGrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { /** The erased {@code java.util.SequencedCollection} type. */ - private final TypeMirror sequencedCollectionErasure; + private final @Nullable TypeMirror sequencedCollectionErasure; /** The {@code @}{@link MaybeSeqGrowable} qualifier. */ private final AnnotationMirror MAYBE_SEQ_GROWABLE; @@ -55,8 +56,12 @@ public class SeqGrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFacto public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); + TypeElement sequencedCollectionElement = + elements.getTypeElement("java.util.SequencedCollection"); this.sequencedCollectionErasure = - types.erasure(elements.getTypeElement("java.util.SequencedCollection").asType()); + sequencedCollectionElement == null + ? null + : types.erasure(sequencedCollectionElement.asType()); this.MAYBE_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeSeqGrowable.class); this.SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, SeqGrowable.class); this.SEQ_UNGROWABLE = AnnotationBuilder.fromClass(elements, SeqUngrowable.class); @@ -139,7 +144,8 @@ protected ParameterizedExecutableType methodFromUse( * @return true if {@code type} structurally cannot support sequenced grow operations */ private boolean typeCannotSeqGrow(TypeMirror type) { - return type.getKind() != TypeKind.DECLARED + return sequencedCollectionErasure == null + || type.getKind() != TypeKind.DECLARED || !TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types); } } From c31c83f630120516e66aa374d650d45375ea6d8d Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Jun 2026 16:20:20 -0700 Subject: [PATCH 208/308] document SeqGrowAnnotatedTypeFactory. Co-authored-by: Copilot --- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index ad37c595fe4d..f66b0ce0467c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -101,6 +101,14 @@ protected Set> createSupportedTypeQualifiers() { PolySeqGrowable.class)); } + /** + * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for + * aliases whose meaning depends on the annotated type. + * + *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types + * that structurally cannot support sequenced grow operations, such as {@code Iterator}, their + * SeqGrow component canonicalizes to {@code @MaybeSeqGrowable}. + */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { From e9348b1dcd76b8a04e1699d173104554135fbb53 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 1 Jun 2026 17:14:52 -0700 Subject: [PATCH 209/308] Typo fix --- .../checker/modifiability/qual/IteratorPolyMod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index ec4c8d2b8de1..eab5c7afc93e 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -9,7 +9,7 @@ /** * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} and {@code - * listIterator()} methods preserves the ability to call modification methods in its iterator. + * listIterator()} methods preserve the ability to call modification methods in its iterator. * *

For example, if collection {@code c} has type {@code @Shrinkable}, then {@code c.iterator()} * also has type {@code @Shrinkable}. If list {@code l} has type {@code @Replaceable}, then {@code From db1f39655ad8b589571b3b8ad4f40e5162eb962c Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Jun 2026 21:13:46 -0700 Subject: [PATCH 210/308] fixed seqGrow logic to add Deque. --- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index f66b0ce0467c..ef09c2630df7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -35,6 +35,9 @@ public class SeqGrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFacto /** The erased {@code java.util.SequencedCollection} type. */ private final @Nullable TypeMirror sequencedCollectionErasure; + /** The erased {@code java.util.Deque} type. */ + private final TypeMirror dequeErasure; + /** The {@code @}{@link MaybeSeqGrowable} qualifier. */ private final AnnotationMirror MAYBE_SEQ_GROWABLE; @@ -62,6 +65,7 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { sequencedCollectionElement == null ? null : types.erasure(sequencedCollectionElement.asType()); + this.dequeErasure = types.erasure(elements.getTypeElement("java.util.Deque").asType()); this.MAYBE_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeSeqGrowable.class); this.SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, SeqGrowable.class); this.SEQ_UNGROWABLE = AnnotationBuilder.fromClass(elements, SeqUngrowable.class); @@ -114,9 +118,9 @@ public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (tm != null) { if (areSameByClass(annotation, Modifiable.class)) { - return typeCannotSeqGrow(tm) ? MAYBE_SEQ_GROWABLE : SEQ_GROWABLE; + return typeCanSeqGrow(tm) ? SEQ_GROWABLE : MAYBE_SEQ_GROWABLE; } else if (areSameByClass(annotation, Unmodifiable.class)) { - return typeCannotSeqGrow(tm) ? MAYBE_SEQ_GROWABLE : SEQ_UNGROWABLE; + return typeCanSeqGrow(tm) ? SEQ_UNGROWABLE : MAYBE_SEQ_GROWABLE; } } if (areSameByClass(annotation, MaybeModifiable.class) @@ -144,16 +148,21 @@ protected ParameterizedExecutableType methodFromUse( } /** - * Returns true if {@code type} structurally cannot support sequenced grow operations. + * Returns true if {@code type} structurally can support sequenced grow operations. * - *

Only sequenced collections can support sequenced grow operations. + *

Only sequenced collections can support sequenced grow operations. On JDKs before Java 21, + * {@code SequencedCollection} is not present, but {@code Deque} still supports first/last + * insertion operations. * * @param type the type to test - * @return true if {@code type} structurally cannot support sequenced grow operations + * @return true if {@code type} structurally can support sequenced grow operations */ - private boolean typeCannotSeqGrow(TypeMirror type) { - return sequencedCollectionErasure == null - || type.getKind() != TypeKind.DECLARED - || !TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types); + private boolean typeCanSeqGrow(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return (sequencedCollectionErasure != null + && TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types)) + || TypesUtils.isErasedSubtype(type, dequeErasure, types); } } From 887ecbf744313b5beba278c8e780357fc7475fbb Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 1 Jun 2026 22:41:55 -0700 Subject: [PATCH 211/308] add sequencedMap to @SeqGrowable annotation. --- .../modifiability/qual/MaybeSeqGrowable.java | 8 +- .../modifiability/qual/SeqGrowable.java | 4 +- .../modifiability/qual/SeqUngrowable.java | 4 +- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 14 ++- .../CollectionsModifiableTest.java | 3 +- .../modifiability/SeqGrowableMapTest.java | 97 +++++++++++++++++++ .../tests/modifiability/ThrowsUOETest.java | 20 ++-- docs/manual/modifiability-checker.tex | 44 ++++++--- .../tests/all-systems/GenericsEnclosing.java | 7 +- 9 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 checker/tests/modifiability/SeqGrowableMapTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java index edc800772346..27e7feb3dc8c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeSeqGrowable.java @@ -9,10 +9,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the sequenced-grow hierarchy. The collection may or may not support - * positional grow operations such as {@code addFirst} and {@code addLast}. The checker - * conservatively issues an error wherever a sequenced-grow operation is called on a - * {@code @MaybeSeqGrowable} expression. + * The top qualifier in the sequenced-grow hierarchy. The collection or map may or may not support + * positional operations such as {@code addFirst}, {@code addLast}, {@code putFirst}, and {@code + * putLast}. The checker conservatively issues an error wherever a sequenced-grow operation is + * called on a {@code @MaybeSeqGrowable} expression. * *

This is the default qualifier for unannotated types in the sequenced-grow hierarchy. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java index ce329fe46e61..3714fe5909d9 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqGrowable.java @@ -9,8 +9,8 @@ /** * Calling sequenced-grow operations such as {@code addFirst}, {@code addLast}, {@code offerFirst}, - * {@code offerLast}, and {@code push} on this collection will not throw {@link - * UnsupportedOperationException}. + * {@code offerLast}, {@code push}, {@code putFirst}, and {@code putLast} on this collection or map + * will not throw {@link UnsupportedOperationException}. * *

No guarantees are made about ordinary grow, shrink, or replace operations. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java index 26c67f1cfeb8..ff6c4008ed1f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/SeqUngrowable.java @@ -9,8 +9,8 @@ /** * Calling sequenced-grow operations such as {@code addFirst}, {@code addLast}, {@code offerFirst}, - * {@code offerLast}, and {@code push} on this collection will throw {@link - * UnsupportedOperationException}. + * {@code offerLast}, {@code push}, {@code putFirst}, and {@code putLast} on this collection or map + * will throw {@link UnsupportedOperationException}. * *

No guarantees are made about ordinary grow, shrink, or replace operations. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index ef09c2630df7..ee49bdc7ab25 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -35,6 +35,9 @@ public class SeqGrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFacto /** The erased {@code java.util.SequencedCollection} type. */ private final @Nullable TypeMirror sequencedCollectionErasure; + /** The erased {@code java.util.SequencedMap} type. */ + private final @Nullable TypeMirror sequencedMapErasure; + /** The erased {@code java.util.Deque} type. */ private final TypeMirror dequeErasure; @@ -65,6 +68,9 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { sequencedCollectionElement == null ? null : types.erasure(sequencedCollectionElement.asType()); + TypeElement sequencedMapElement = elements.getTypeElement("java.util.SequencedMap"); + this.sequencedMapErasure = + sequencedMapElement == null ? null : types.erasure(sequencedMapElement.asType()); this.dequeErasure = types.erasure(elements.getTypeElement("java.util.Deque").asType()); this.MAYBE_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeSeqGrowable.class); this.SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, SeqGrowable.class); @@ -150,9 +156,9 @@ protected ParameterizedExecutableType methodFromUse( /** * Returns true if {@code type} structurally can support sequenced grow operations. * - *

Only sequenced collections can support sequenced grow operations. On JDKs before Java 21, - * {@code SequencedCollection} is not present, but {@code Deque} still supports first/last - * insertion operations. + *

Only sequenced collections and sequenced maps can support sequenced grow operations. On JDKs + * before Java 21, {@code SequencedCollection} and {@code SequencedMap} are not present, but + * {@code Deque} still supports first/last insertion operations. * * @param type the type to test * @return true if {@code type} structurally can support sequenced grow operations @@ -163,6 +169,8 @@ private boolean typeCanSeqGrow(TypeMirror type) { } return (sequencedCollectionErasure != null && TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types)) + || (sequencedMapErasure != null + && TypesUtils.isErasedSubtype(type, sequencedMapErasure, types)) || TypesUtils.isErasedSubtype(type, dequeErasure, types); } } diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index 70abc93f2640..bbfb0402a693 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -295,7 +295,8 @@ void testUnmodifiableWrappers() { // :: error: [method.invocation] unmodSS.add("a"); - @Modifiable SortedMap sm = new TreeMap<>(); + @Growable @Shrinkable @Replaceable @SeqUngrowable + SortedMap sm = new TreeMap<>(); @Unmodifiable SortedMap unmodSM = Collections.unmodifiableSortedMap(sm); // :: error: [method.invocation] unmodSM.put("a", "b"); diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java new file mode 100644 index 000000000000..0096eac216d7 --- /dev/null +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -0,0 +1,97 @@ +// @below-java21-jdk-skip-test + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.SequencedMap; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentSkipListMap; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; + +public class SeqGrowableMapTest { + + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] :: error: + // [declaration.inconsistent.with.implements.clause] + abstract static @SeqGrowable class UserDefinedSortedMap implements SortedMap {} + + void linkedHashMapSupportsSequencedMapGrow() { + @Modifiable LinkedHashMap modifiable = new LinkedHashMap<>(); + modifiable.putFirst("a", 1); + modifiable.putLast("b", 2); + + @SeqGrowable LinkedHashMap seqGrowable = new LinkedHashMap<>(); + seqGrowable.putFirst("c", 3); + seqGrowable.putLast("d", 4); + + @SeqGrowable SequencedMap sequenced = new LinkedHashMap<>(); + sequenced.putFirst("e", 5); + sequenced.putLast("f", 6); + } + + void ordinaryMapsCanStillUseModifiable() { + @Modifiable HashMap map = new HashMap<>(); + map.put("a", 1); + map.remove("a"); + map.put("b", 2); + map.replace("b", 3); + } + + void maybeSeqGrowableIsRejected( + SequencedMap unknown, + @MaybeSeqGrowable SequencedMap maybeSequenced) { + // :: error: [method.invocation] + unknown.putFirst("a", 1); + // :: error: [method.invocation] + maybeSequenced.putLast("b", 2); + } + + void ordinaryGrowAndReplaceDoNotImplySeqGrow( + @Growable @Replaceable SequencedMap growReplace) { + growReplace.put("a", 1); + // :: error: [method.invocation] + growReplace.putFirst("b", 2); + // :: error: [method.invocation] + growReplace.putLast("c", 3); + } + + void sortedMapsAreSeqUngrowable() { + @Growable @Shrinkable @Replaceable @SeqUngrowable + TreeMap treeMap = new TreeMap<>(); + treeMap.put("a", 1); + treeMap.remove("a"); + treeMap.put("b", 2); + treeMap.replace("b", 3); + // :: error: [usage.throws.uoe] :: error: [method.invocation] + treeMap.putFirst("c", 4); + + @Growable @Shrinkable @Replaceable @SeqUngrowable + ConcurrentSkipListMap concurrentSkipListMap = new ConcurrentSkipListMap<>(); + concurrentSkipListMap.put("a", 1); + concurrentSkipListMap.remove("a"); + concurrentSkipListMap.put("b", 2); + concurrentSkipListMap.replace("b", 3); + // :: error: [usage.throws.uoe] :: error: [method.invocation] + concurrentSkipListMap.putLast("c", 4); + + // :: error: [assignment] + @SeqGrowable TreeMap seqGrowableTreeMap = new TreeMap<>(); + + // :: error: [assignment] + @Modifiable TreeMap modifiableTreeMap = new TreeMap<>(); + + @SeqGrowable + ConcurrentSkipListMap seqGrowableConcurrentSkipListMap = + // :: error: [assignment] + new ConcurrentSkipListMap<>(); + + @Modifiable ConcurrentSkipListMap modifiableConcurrentSkipListMap = + // :: error: [assignment] + new ConcurrentSkipListMap<>(); + } +} diff --git a/checker/tests/modifiability/ThrowsUOETest.java b/checker/tests/modifiability/ThrowsUOETest.java index f3fa5e2a029f..e82ea530c84e 100644 --- a/checker/tests/modifiability/ThrowsUOETest.java +++ b/checker/tests/modifiability/ThrowsUOETest.java @@ -31,42 +31,42 @@ void testImplementation(TreeSet ts) { } void testSortedMap(SortedMap m) { - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putLast("baz", "qux"); } void testImplementation(TreeMap tm) { - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] tm.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] tm.putLast("baz", "qux"); } void testNavigableMap(NavigableMap m) { - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putLast("baz", "qux"); } void testConcurrentNavigableMap(ConcurrentNavigableMap m) { - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putLast("baz", "qux"); } void testConcurrentSkipListMap(ConcurrentSkipListMap m) { - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] + // :: error: [usage.throws.uoe] :: error: [method.invocation] m.putLast("baz", "qux"); } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 4d1ee2a980ca..29d5fa297fed 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -65,15 +65,17 @@ as \code{size}, \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be called on any collection, whether modifiable or unmodifiable. -Some collections may be modified in some ways but not in others. -A collection can be modified in four different ways: +Some collections and maps may be modified in some ways but not in others. +A collection or map can be modified in four different ways: \textbf{Grow}, \textbf{SeqGrow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements (\, \, \, \ldots). \item \textbf{SeqGrow}: A sequenced collection can grow by adding new elements at the beginning or end (\, \, - \, \, \, \ldots). + \, \, \, \ldots), and a sequenced map can + insert or replace mappings at the beginning or end (\, + \). \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing @@ -218,23 +220,29 @@ \begin{description} \item[\refqualclass{checker/modifiability/qual}{MaybeSeqGrowable}] The top qualifier in the SeqGrow hierarchy. -The checker cannot determine whether this collection supports sequenced grow +The checker cannot determine whether this collection or map supports sequenced grow operations. -Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this -collection \emph{may} throw \UnsupportedOperationException. +Calling sequenced grow operations such as \code{addFirst}, \code{addLast}, +\code{putFirst}, and \code{putLast} on this collection or map \emph{may} +throw \UnsupportedOperationException. This is the default qualifier for unannotated types in the SeqGrow hierarchy. \item[\refqualclass{checker/modifiability/qual}{SeqGrowable}] -Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this -collection \emph{will not} throw \UnsupportedOperationException. +Sequenced grow operations, such as \code{addFirst} and \code{addLast} for +collections or \code{putFirst} and \code{putLast} for maps, \emph{will not} +throw \UnsupportedOperationException. \item[\refqualclass{checker/modifiability/qual}{SeqUngrowable}] -Calling sequenced grow operations such as \code{addFirst} and \code{addLast} on this -collection \emph{will} throw \UnsupportedOperationException. +Sequenced grow operations, such as \code{addFirst} and \code{addLast} for +collections or \code{putFirst} and \code{putLast} for maps, \emph{will} +throw \UnsupportedOperationException. For example, \ and \ support ordinary \ but throw \UnsupportedOperationException\ for explicit positional insertion with \ and \. +Similarly, \ and \ support ordinary \ +but throw \UnsupportedOperationException\ for explicit positional updates +with \ and \. \item[\refqualclass{checker/modifiability/qual}{BottomSeqGrowable}] The bottom qualifier in the SeqGrow hierarchy. Programmers should rarely write it. @@ -394,6 +402,10 @@ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ \<@Modifiable> & \ & = & \<@Growable> & \<@SeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@SeqUngrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@Replaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@Unreplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@SeqGrowable> & \<@Shrinkable> & \<@Replaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@SeqUngrowable> & \<@Unshrinkable> & \<@Unreplaceable> & \ \\ \<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ \<@Modifiable> & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ @@ -405,11 +417,14 @@ qualifier. For example, \sunjavadoc{java.base/java/util/Queue.html}{\code{Queue}} has \ and \ methods but no replacement methods. -It is not a subtype of \, so it also has no -\ or \ methods. By contrast, \ is a +It is not a subtype of \ or \, so it also has no +\, \, \, or \ methods. By contrast, \ is a \, so its seq-grow capability is relevant; the JDK annotations mark it \<@SeqUngrowable> because its \ and -\ methods always throw \UnsupportedOperationException. +\ methods always throw \UnsupportedOperationException. Likewise, +\ is a \, so the JDK annotations mark it +\<@SeqUngrowable> because its \ and \ methods always throw +\UnsupportedOperationException. Although no object of class \ supports replace operations, @@ -529,7 +544,8 @@ % LocalWords: MaybeReplaceable MaybeSeqGrowable growable seq ListIterator % LocalWords: PolyModifiable PolySeqGrowable addFirst synchronizedList % LocalWords: PolyShrinkable PolyGrowable PolyReplaceable ThrowsUOE SeqGrow -% LocalWords: addLast offerFirst offerLast unreplaceable seq-growability +% LocalWords: addLast offerFirst offerLast putFirst putLast unreplaceable seq-growability +% LocalWords: SequencedMap SortedMap % LocalWords: SeqGrowable SeqUngrowable BottomSeqGrowable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable diff --git a/framework/tests/all-systems/GenericsEnclosing.java b/framework/tests/all-systems/GenericsEnclosing.java index 3c5d6811e77e..8e809027b260 100644 --- a/framework/tests/all-systems/GenericsEnclosing.java +++ b/framework/tests/all-systems/GenericsEnclosing.java @@ -1,5 +1,7 @@ import java.util.TreeMap; -import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; /** * Resolution of outer classes must take substitution of generic types into account. Thanks to EMS @@ -8,7 +10,8 @@ *

Also see regex/GenericsEnclosing for a test case for the Regex Checker. */ @SuppressWarnings("modifiability:annotation.unverified") -public @Modifiable class GenericsEnclosing extends TreeMap { +public @Growable @Replaceable @SeqUngrowable class GenericsEnclosing + extends TreeMap { class Inner { void foo() { put("string", "string".toString()); From 397dfc3d8d75948540baf72b2a897199e683dda2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 2 Jun 2026 14:23:21 -0700 Subject: [PATCH 212/308] optimize the implementation of deteching unmodparam.location error. --- .../modifiability/ModifiabilityVisitor.java | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index f8d94a99572b..1f6963c5227d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -4,9 +4,12 @@ import com.sun.source.tree.ClassTree; 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 com.sun.source.util.TreeScanner; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -43,6 +46,9 @@ public class ModifiabilityVisitor extends BaseTypeVisitor> allowedUnmodifiableParamAnnotations = new ArrayDeque<>(); + /** * Create a ModifiabilityVisitor. * @@ -73,10 +79,32 @@ public void processClassTree(ClassTree classTree) { } } + /** + * Collects the {@code @UnmodifiableParam} annotations that are permitted in this method's formal + * and receiver parameters before visiting the method body. {@link #visitAnnotation} uses the + * stack entry to distinguish allowed parameter annotations from disallowed uses elsewhere in the + * same method. + */ + @Override + public void processMethodTree(String className, MethodTree tree) { + Set allowedAnnotations = new HashSet<>(); + for (VariableTree parameter : tree.getParameters()) { + allowedAnnotations.addAll(unmodifiableParamAnnotations(parameter)); + } + VariableTree receiverParameter = tree.getReceiverParameter(); + if (receiverParameter != null) { + allowedAnnotations.addAll(unmodifiableParamAnnotations(receiverParameter)); + } + allowedUnmodifiableParamAnnotations.push(allowedAnnotations); + super.processMethodTree(className, tree); + allowedUnmodifiableParamAnnotations.pop(); + } + @Override public Void visitAnnotation(AnnotationTree tree, Void p) { if (shouldCheckUnmodifiableParamLocation() && isUnmodifiableParamAnnotation(tree)) { - if (!isWithinAllowedUnmodifiableParamLocation()) { + if (allowedUnmodifiableParamAnnotations.isEmpty() + || !allowedUnmodifiableParamAnnotations.peek().contains(tree)) { checker.reportError(tree, "unmodparam.location"); } } @@ -214,38 +242,35 @@ private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { } /** - * Returns true if the current annotation path is inside a method/constructor parameter type or - * explicit receiver parameter type. + * Returns all {@code @UnmodifiableParam} annotations written on a formal or receiver parameter's + * type. An annotation before the parameter type may appear in the parameter's modifiers, while an + * annotation inside a generic or array type appears in the parameter's type tree. * - * @return true if {@code @UnmodifiableParam} is allowed at the current location + * @param parameter a formal or receiver parameter + * @return the {@code @UnmodifiableParam} annotation trees that are allowed by this parameter + * location */ - @SuppressWarnings("interning:not.interned") // AST node comparison - private boolean isWithinAllowedUnmodifiableParamLocation() { - // Find the declaration that contains the annotation, if any. - TreePath path = getCurrentPath(); - TreePath variablePath = null; - while (path != null) { - if (path.getLeaf() instanceof VariableTree) { - variablePath = path; - break; - } - path = path.getParentPath(); - } + private Set unmodifiableParamAnnotations(VariableTree parameter) { + Set annotations = new HashSet<>(); + TreeScanner scanner = + new TreeScanner<>() { + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + if (isUnmodifiableParamAnnotation(tree)) { + annotations.add(tree); + } + return super.visitAnnotation(tree, p); + } + }; - if (variablePath == null || variablePath.getParentPath() == null) { - return false; + // Scan both javac locations for parameter type annotations. + // search for method(@UnmodifiableParam List<> param) + scanner.scan(parameter.getModifiers().getAnnotations(), null); + if (parameter.getType() != null) { + // search for method(List<@UnmodifiableParam List<>> param) + scanner.scan(parameter.getType(), null); } - - // Ordinary and receiver parameters are represented as VariableTrees under a MethodTree. - Tree parent = variablePath.getParentPath().getLeaf(); - if (!(parent instanceof MethodTree methodTree)) { - return false; - } - - // Allow nested annotations anywhere within an allowed parameter's type. - VariableTree variable = (VariableTree) variablePath.getLeaf(); - return methodTree.getParameters().contains(variable) - || methodTree.getReceiverParameter() == variable; + return annotations; } // Suppresses the framework's "constructor result must be TOP" check. From 927fcf9b07509623d0f1d72c55f00f8ff9c5c768 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 2 Jun 2026 14:41:10 -0700 Subject: [PATCH 213/308] fixed unmodParamLocationTest. --- checker/tests/modifiability/UnmodParamLocationTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/checker/tests/modifiability/UnmodParamLocationTest.java b/checker/tests/modifiability/UnmodParamLocationTest.java index 6a7ab643bda7..973ba8c68496 100644 --- a/checker/tests/modifiability/UnmodParamLocationTest.java +++ b/checker/tests/modifiability/UnmodParamLocationTest.java @@ -18,7 +18,10 @@ void method(@UnmodifiableParam List parameter) {} void nestedParameter(List<@UnmodifiableParam List> parameter) {} - void receiver(@UnmodifiableParam UnmodParamLocationTest this) {} + @SuppressWarnings("modifiability:annotation.unverified") + abstract static class RelevantReceiver implements List { + void receiver(@UnmodifiableParam RelevantReceiver this) {} + } // :: error: [unmodparam.location] @UnmodifiableParam List returnType() { From cde29f8512b5dd576f1722582df12e9bbc739c8c Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 2 Jun 2026 22:33:45 -0700 Subject: [PATCH 214/308] restrict the modifiability.annotation.unverified warning to class/constructors with explicit @Mod/@Unmod annotaitons. --- .../modifiability/ModifiabilityVisitor.java | 91 ++++++++++++++- .../CustomModifiabilityAnnotationWarning.java | 109 ++++++++++++++++-- .../modifiability/IteratorPrecisionTest.java | 5 - .../modifiability/SetRefinementTest.java | 1 - .../tests/all-systems/InferTypeArgs3.java | 1 - framework/tests/all-systems/Issue6810.java | 1 - .../all-systems/java8inference/Issue1715.java | 1 - .../java8inference/MemRefInfere.java | 1 - 8 files changed, 191 insertions(+), 19 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 1f6963c5227d..79954e194439 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -4,6 +4,7 @@ import com.sun.source.tree.ClassTree; 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.TreeScanner; import java.util.ArrayDeque; @@ -11,6 +12,7 @@ import java.util.HashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; @@ -49,6 +51,10 @@ public class ModifiabilityVisitor extends BaseTypeVisitor> allowedUnmodifiableParamAnnotations = new ArrayDeque<>(); + /** Package that contains the modifiability type-use qualifiers. */ + private static final String MODIFIABILITY_QUAL_PACKAGE = + "org.checkerframework.checker.modifiability.qual"; + /** * Create a ModifiabilityVisitor. * @@ -67,12 +73,22 @@ public ModifiabilityVisitor(BaseTypeChecker checker) { atypeFactory.getElementUtils().getTypeElement("java.util.Iterator").asType()); } + /** + * Processes a class declaration and reports an unverified modifiability warning only when a + * source-defined collection-like type explicitly claims a concrete modifiability qualifier on the + * class or one of its constructors. Defaulted/unannotated types and Maybe* annotations do not + * make a verifiable modifiability claim. + * + * @param classTree the class declaration to process + */ @Override public void processClassTree(ClassTree classTree) { super.processClassTree(classTree); if (shouldCheckCustomModifiabilityAnnotation()) { TypeElement typeElement = TreeUtils.elementFromDeclaration(classTree); - if (typeElement != null && isCollectionFromSourceCode(typeElement)) { + if (typeElement != null + && isCollectionFromSourceCode(typeElement) + && hasExplicitWarningModifiabilityAnnotation(classTree)) { checker.reportWarning( classTree, "modifiability.annotation.unverified", typeElement.getQualifiedName()); } @@ -241,6 +257,79 @@ private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { return annotation != null && atypeFactory.areSameByClass(annotation, UnmodifiableParam.class); } + /** + * Returns true if {@code classTree} explicitly writes a warning-worthy modifiability qualifier on + * the class declaration or on any constructor. + * + * @param classTree a class declaration + * @return true if a warning-worthy modifiability annotation is written on the class or + * constructor + */ + private boolean hasExplicitWarningModifiabilityAnnotation(ClassTree classTree) { + // write explicit modifianility annotations on the class + if (hasWarningModifiabilityAnnotation(classTree.getModifiers().getAnnotations())) { + return true; + } + + // write explicit modifiability annotations on any constructor + for (Tree member : classTree.getMembers()) { + if (member instanceof MethodTree methodTree + && TreeUtils.isConstructor(methodTree) + && hasWarningModifiabilityAnnotation(methodTree.getModifiers().getAnnotations())) { + return true; + } + } + return false; + } + + /** + * Returns true if the list {@code annotations} contains any explicit non-maybe modifiability + * qualifier. + * + *

Iterator throw the list of annotations and call isWarningModifiablityAnnotation(annotation) + * + * @param annotations annotation trees to inspect + * @return true if one annotation should trigger an unverified modifiability warning + */ + private boolean hasWarningModifiabilityAnnotation( + Iterable annotations) { + for (AnnotationTree annotationTree : annotations) { + AnnotationMirror annotation = TreeUtils.annotationFromAnnotationTree(annotationTree); + if (annotation != null && isWarningModifiabilityAnnotation(annotation)) { + return true; + } + } + return false; + } + + /** + * Returns true if {@code annotation} is a non-maybe modifiability qualifier that should trigger + * the unverified custom modifiability warning. + * + * @param annotation an annotation mirror + * @return true if {@code annotation} should trigger a warning + */ + private boolean isWarningModifiabilityAnnotation(AnnotationMirror annotation) { + Element element = annotation.getAnnotationType().asElement(); + if (!(element instanceof TypeElement typeElement)) { + return false; + } + + // Only annotations in the modifiability qualifier package are relevant. + String qualifiedName = typeElement.getQualifiedName().toString(); + if (!qualifiedName.startsWith(MODIFIABILITY_QUAL_PACKAGE + ".")) { + return false; + } + + // Maybe* annotations and @UnmodifiableParam are unknown/top-like, so they do not warn. + String simpleName = typeElement.getSimpleName().toString(); + if (simpleName.startsWith("Maybe") || simpleName.equals("UnmodifiableParam")) { + return false; + } + + return true; + } + /** * Returns all {@code @UnmodifiableParam} annotations written on a formal or receiver parameter's * type. An annotation before the parameter type may appear in the parameter's modifiers, while an diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index cd606d2f919c..64effead0dd1 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -3,10 +3,18 @@ import java.util.Iterator; import java.util.ListIterator; import java.util.Map; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; public class CustomModifiabilityAnnotationWarning { - // :: warning: [modifiability.annotation.unverified] static class ExtendsAbstractList extends AbstractList { @Override public String get(int index) { @@ -19,22 +27,105 @@ public int size() { } } - // :: warning: [modifiability.annotation.unverified] abstract static class DirectCollection implements Collection {} - // :: warning: [modifiability.annotation.unverified] abstract static class DirectMap implements Map {} - // :: warning: [modifiability.annotation.unverified] abstract static class DirectIterator implements Iterator {} - // :: warning: [modifiability.annotation.unverified] abstract static class DirectListIterator implements ListIterator {} static class NonModifiabilityType {} + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + static @Modifiable class ClassLevelModifiableList extends AbstractList { + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + abstract static @Growable class ClassLevelGrowableCollection implements Collection {} + + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + abstract static @Unmodifiable class ClassLevelUnmodifiableMap implements Map {} + + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator + implements Iterator {} + + // :: warning: [modifiability.annotation.unverified] + static class ConstructorLevelShrinkableList extends AbstractList { + // :: error: [super.invocation] + @Shrinkable ConstructorLevelShrinkableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + static @MaybeModifiable class ClassLevelMaybeModifiableList extends AbstractList { + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + abstract static @MaybeGrowable class ClassLevelMaybeGrowableCollection + implements Collection {} + + abstract static @MaybeIteratorPolyMod class ClassLevelMaybeIteratorPolyModIterator + implements Iterator {} + + static class ConstructorLevelMaybeSeqGrowableList extends AbstractList { + @MaybeSeqGrowable + ConstructorLevelMaybeSeqGrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + static class MemberAnnotationDoesNotCount extends AbstractList { + @Modifiable AbstractList field; + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + @SuppressWarnings("modifiability:annotation.unverified") - static class SuppressedList extends AbstractList { + // :: error: [super.invocation] + static @Modifiable class SuppressedList extends AbstractList { @Override public String get(int index) { return "value"; @@ -47,8 +138,10 @@ public int size() { } @SuppressWarnings("modifiability:annotation.unverified") - abstract static class SuppressedMap implements Map {} + // :: error: [super.invocation] + abstract static @Unmodifiable class SuppressedMap implements Map {} @SuppressWarnings("modifiability:annotation.unverified") - abstract static class SuppressedIterator implements Iterator {} + // :: error: [super.invocation] + abstract static @IteratorPolyMod class SuppressedIterator implements Iterator {} } diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 379c88f9f772..4febfddcfbf4 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -318,11 +318,9 @@ void copyOnWriteDirectOverridesRemainUsableButInheritedIteratorBasedApisDoNot() asList.replaceAll(s -> s); } - // :: warning: [modifiability.annotation.unverified] static class IteratorRemoveUnsupportedCollection extends AbstractCollection { @Override public Iterator iterator() { - // :: warning: [modifiability.annotation.unverified] return new Iterator<>() { private boolean hasNext = true; @@ -348,7 +346,6 @@ public int size() { } } - // :: warning: [modifiability.annotation.unverified] static class IteratorRemoveUnsupportedSet extends AbstractSet { @Override public Iterator iterator() { @@ -361,11 +358,9 @@ public int size() { } } - // :: warning: [modifiability.annotation.unverified] static class IteratorMutationUnsupportedList extends AbstractSequentialList { @Override public ListIterator listIterator(int index) { - // :: warning: [modifiability.annotation.unverified] return new ListIterator<>() { private int cursor = index; diff --git a/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java index 912ed110c3ea..344eaa5a1588 100644 --- a/checker/tests/modifiability/SetRefinementTest.java +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -103,7 +103,6 @@ public void arrayAccessReceiverSideEffect(List other) { // effects, so the behavior is correct regardless of @DoesNotUnrefineReceiver. // ========================================================== - // :: warning: [modifiability.annotation.unverified] static class ThisListReceiver extends ArrayList { public void update(@Modifiable ThisListReceiver this, List other) { diff --git a/framework/tests/all-systems/InferTypeArgs3.java b/framework/tests/all-systems/InferTypeArgs3.java index 58ffdb32f349..3e5a51f26ee6 100644 --- a/framework/tests/all-systems/InferTypeArgs3.java +++ b/framework/tests/all-systems/InferTypeArgs3.java @@ -12,7 +12,6 @@ void test2() { java.util.Arrays.asList(i, ""); } - @SuppressWarnings("modifiability:annotation.unverified") void foo() { new HashSet<>(Arrays.asList(new Object())); new HashSet(Arrays.asList(new Object())) {}; diff --git a/framework/tests/all-systems/Issue6810.java b/framework/tests/all-systems/Issue6810.java index 09897ff797c0..40a6b1c34389 100644 --- a/framework/tests/all-systems/Issue6810.java +++ b/framework/tests/all-systems/Issue6810.java @@ -4,7 +4,6 @@ public class Issue6810 { static class Box> {} - @SuppressWarnings("modifiability:annotation.unverified") abstract static class BoxSet> implements Set {} static > void intersect2( diff --git a/framework/tests/all-systems/java8inference/Issue1715.java b/framework/tests/all-systems/java8inference/Issue1715.java index b7f8b120eb02..948329fb757b 100644 --- a/framework/tests/all-systems/java8inference/Issue1715.java +++ b/framework/tests/all-systems/java8inference/Issue1715.java @@ -23,7 +23,6 @@ static Observable just(T param) { } } - @SuppressWarnings("modifiability:annotation.unverified") public abstract static class ImmutableList implements List { public static ImmutableList of(E element) { diff --git a/framework/tests/all-systems/java8inference/MemRefInfere.java b/framework/tests/all-systems/java8inference/MemRefInfere.java index 53ecb78e6a97..0ddd3bd9060e 100644 --- a/framework/tests/all-systems/java8inference/MemRefInfere.java +++ b/framework/tests/all-systems/java8inference/MemRefInfere.java @@ -8,7 +8,6 @@ import java.util.stream.Collector; import java.util.stream.Collectors; -@SuppressWarnings("modifiability:annotation.unverified") public abstract class MemRefInfere implements Map, Serializable { public static MemRefInfere copyOf(Map map) { throw new RuntimeException(); From 713b171d342969ea197789ad9fda5b30ee37dbae Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 3 Jun 2026 13:20:10 -0700 Subject: [PATCH 215/308] remvoed throwsUOE. --- .../checker/modifiability/qual/ThrowsUOE.java | 17 --------- .../modifiability/ModifiabilityVisitor.java | 33 ----------------- .../modifiability/replace/ReplaceVisitor.java | 6 --- .../modifiability/seqgrow/SeqGrowVisitor.java | 6 --- .../modifiability/shrink/ShrinkVisitor.java | 6 --- .../checker/modifiability/messages.properties | 3 -- .../modifiability/SeqGrowableMapTest.java | 4 +- ...rowsUOETest.java => SeqGrowableTest2.java} | 37 +++++++++++-------- docs/manual/modifiability-checker.tex | 9 +---- 9 files changed, 25 insertions(+), 96 deletions(-) delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java delete mode 100644 checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties rename checker/tests/modifiability/{ThrowsUOETest.java => SeqGrowableTest2.java} (57%) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java deleted file mode 100644 index 1275077f2023..000000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUOE.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.checkerframework.checker.modifiability.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; - -/** - * The annotated method always throws {@link UnsupportedOperationException} when called. - * - * @checker_framework.manual #modifiability-checker Modifiability Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -public @interface ThrowsUOE {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 79954e194439..707c4cfbc261 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -2,7 +2,6 @@ import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; @@ -16,7 +15,6 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.modifiability.qual.ThrowsUOE; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -32,7 +30,6 @@ *

This class contains logic shared across all three sub-checkers: * *

    - *
  • Reporting errors for invocations of methods annotated with {@link ThrowsUOE}. *
  • Suppressing the "constructor result must be TOP" check, since collection constructors may * legitimately produce {@code @Modifiable}. *
@@ -127,21 +124,6 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { return super.visitAnnotation(tree, p); } - @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - // Methods annotated @ThrowsUOE always throw UnsupportedOperationException when called. - // When running as part of the aggregate ModifiabilityChecker, only the GrowChecker - // reports this error (shouldCheckThrowsUOE() returns false in the other two) to avoid - // tripling the diagnostic. When running a sub-checker standalone, each reports it. - if (shouldCheckThrowsUOE()) { - ExecutableElement method = TreeUtils.elementFromUse(node); - if (atypeFactory.getDeclAnnotation(method, ThrowsUOE.class) != null) { - checker.reportError(node, "usage.throws.uoe", method.getSimpleName()); - } - } - return super.visitMethodInvocation(node, p); - } - /** * Returns the positive qualifier for this checker's modifiability hierarchy, such as * {@code @Growable}, {@code @Shrinkable}, or {@code @Replaceable}. @@ -199,21 +181,6 @@ protected boolean checkOverride( return true; } - /** - * Returns true if this checker should report {@code @ThrowsUOE} errors. - * - *

The default is {@code true}. {@link - * org.checkerframework.checker.modifiability.shrink.ShrinkVisitor} and {@link - * org.checkerframework.checker.modifiability.replace.ReplaceVisitor} override this to return - * {@code false} when running under the aggregate {@link ModifiabilityChecker}, so that each - * {@code @ThrowsUOE} call site produces exactly one error rather than three. - * - * @return true if this visitor should report {@code @ThrowsUOE} violations - */ - protected boolean shouldCheckThrowsUOE() { - return true; - } - /** * Returns true if this checker should report {@code @UnmodifiableParam} location errors. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index ec3979311ca7..2b9e44a0db66 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -15,12 +15,6 @@ public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } - @Override - protected boolean shouldCheckThrowsUOE() { - // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. - return checker.getParentChecker() == null; - } - @Override protected boolean shouldCheckUnmodifiableParamLocation() { // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index f78decc266f8..31ec6c49d530 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -15,12 +15,6 @@ public SeqGrowVisitor(BaseTypeChecker checker) { super(checker); } - @Override - protected boolean shouldCheckThrowsUOE() { - // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. - return false; - } - @Override protected boolean shouldCheckUnmodifiableParamLocation() { // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index a723a3b88138..a89b4256336a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -15,12 +15,6 @@ public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } - @Override - protected boolean shouldCheckThrowsUOE() { - // When running under ModifiabilityChecker, GrowChecker handles @ThrowsUOE reporting. - return checker.getParentChecker() == null; - } - @Override protected boolean shouldCheckUnmodifiableParamLocation() { // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location diff --git a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties deleted file mode 100644 index 0b07066578da..000000000000 --- a/checker/src/main/resources/org/checkerframework/checker/modifiability/messages.properties +++ /dev/null @@ -1,3 +0,0 @@ -usage.throws.uoe=Call to %s will always throw UnsupportedOperationException. -unmodparam.location=@UnmodifiableParam may only be written on method/constructor parameters or explicit receiver parameters. -modifiability.annotation.unverified=Modifiability annotations on collection/map/iterator type %s cannot be verified by the checker. diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java index 0096eac216d7..5dc156ac39f7 100644 --- a/checker/tests/modifiability/SeqGrowableMapTest.java +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -67,7 +67,7 @@ void sortedMapsAreSeqUngrowable() { treeMap.remove("a"); treeMap.put("b", 2); treeMap.replace("b", 3); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] treeMap.putFirst("c", 4); @Growable @Shrinkable @Replaceable @SeqUngrowable @@ -76,7 +76,7 @@ void sortedMapsAreSeqUngrowable() { concurrentSkipListMap.remove("a"); concurrentSkipListMap.put("b", 2); concurrentSkipListMap.replace("b", 3); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] concurrentSkipListMap.putLast("c", 4); // :: error: [assignment] diff --git a/checker/tests/modifiability/ThrowsUOETest.java b/checker/tests/modifiability/SeqGrowableTest2.java similarity index 57% rename from checker/tests/modifiability/ThrowsUOETest.java rename to checker/tests/modifiability/SeqGrowableTest2.java index e82ea530c84e..4556f7e30914 100644 --- a/checker/tests/modifiability/ThrowsUOETest.java +++ b/checker/tests/modifiability/SeqGrowableTest2.java @@ -7,14 +7,15 @@ import java.util.TreeSet; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; -public class ThrowsUOETest { +public class SeqGrowableTest2 { void testSortedSet(SortedSet s) { - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] s.addFirst("foo"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] s.addLast("bar"); } @@ -23,50 +24,56 @@ void testImplementation(TreeSet ts) { // We verify that calling them on a concrete implementation also triggers the // warning. - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] ts.addFirst("foo"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] ts.addLast("bar"); } void testSortedMap(SortedMap m) { - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putLast("baz", "qux"); } void testImplementation(TreeMap tm) { - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] tm.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] tm.putLast("baz", "qux"); } void testNavigableMap(NavigableMap m) { - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putLast("baz", "qux"); } void testConcurrentNavigableMap(ConcurrentNavigableMap m) { - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putLast("baz", "qux"); } void testConcurrentSkipListMap(ConcurrentSkipListMap m) { - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putFirst("foo", "bar"); - // :: error: [usage.throws.uoe] :: error: [method.invocation] + // :: error: [method.invocation] m.putLast("baz", "qux"); } + + void f( + // :: error: [annotations.on.use] + @SeqGrowable SortedSet s) { + s.addFirst("x"); + } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 29d5fa297fed..c8646af895f6 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -360,13 +360,6 @@ \begin{description} -\item[\refqualclass{checker/modifiability/qual}{ThrowsUOE}] -When written on a -method or constructor declaration, it indicates that the method always throws -\UnsupportedOperationException. Therefore, any code that calls it is -erroneous. The Modifiability Checker issues an error at every call to a -method annotated with \code{@ThrowsUOE}. - \item[\refqualclass{checker/modifiability/qual}{PreservesModifiability}] When written on a one-argument method declaration, it indicates that the method preserves positive modifiability capabilities from its argument to @@ -543,7 +536,7 @@ % LocalWords: MaybeModifiable UnmodifiableParam Growable MaybeGrowable MaybeShrinkable % LocalWords: MaybeReplaceable MaybeSeqGrowable growable seq ListIterator % LocalWords: PolyModifiable PolySeqGrowable addFirst synchronizedList -% LocalWords: PolyShrinkable PolyGrowable PolyReplaceable ThrowsUOE SeqGrow +% LocalWords: PolyShrinkable PolyGrowable PolyReplaceable SeqGrow % LocalWords: addLast offerFirst offerLast putFirst putLast unreplaceable seq-growability % LocalWords: SequencedMap SortedMap % LocalWords: SeqGrowable SeqUngrowable BottomSeqGrowable Growability shrinkability replacability From e2e35de453775e6a80a27abb1fe5687ddf715c27 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 3 Jun 2026 14:32:05 -0700 Subject: [PATCH 216/308] changed the location of messages.properties --- .../checkerframework/checker/modifiability/messages.properties | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties new file mode 100644 index 000000000000..296143a011c5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -0,0 +1,2 @@ +modifiability.annotation.unverified=Class %s is annotated with a modifiability qualifier, but this checker cannot verify custom collection implementations. +unmodparam.location=@UnmodifiableParam may only be written on a formal parameter or receiver parameter. From e84da5bbf6682ed5348760225031530c21714da7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 3 Jun 2026 21:15:45 -0700 Subject: [PATCH 217/308] edit the test files to include @iteratorPolyMod. --- .../tests/modifiability/AliasCanonicalizationTest.java | 5 +++-- checker/tests/modifiability/BasicModifiabilityTest.java | 5 +++-- checker/tests/modifiability/CollectionModifiableTest.java | 3 ++- .../tests/modifiability/CollectionsModifiableTest.java | 8 ++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/checker/tests/modifiability/AliasCanonicalizationTest.java b/checker/tests/modifiability/AliasCanonicalizationTest.java index a5d410f36be5..30b3420c7e89 100644 --- a/checker/tests/modifiability/AliasCanonicalizationTest.java +++ b/checker/tests/modifiability/AliasCanonicalizationTest.java @@ -5,6 +5,7 @@ import java.util.Queue; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; @@ -40,8 +41,8 @@ void modifiableQueue(@Modifiable Queue queue) { queue.remove(); } - void modifiableCollection(@Modifiable Collection collection) { - @Growable @Shrinkable @MaybeReplaceable Collection canonical = collection; + void modifiableCollection(@Modifiable @IteratorPolyMod Collection collection) { + @Growable @Shrinkable @IteratorPolyMod @MaybeReplaceable Collection canonical = collection; collection.add("a"); collection.remove("a"); } diff --git a/checker/tests/modifiability/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java index 90e4f6352b9d..c26ef4ddbb4e 100644 --- a/checker/tests/modifiability/BasicModifiabilityTest.java +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -86,7 +86,8 @@ void testIteratorPolymorphic( @Modifiable Iterator badIt = unmodList.iterator(); } - void testMutatingBulkOperations(@Modifiable List mod, @Unmodifiable List unmod) { + void testMutatingBulkOperations( + @Modifiable @IteratorPolyMod List mod, @Unmodifiable List unmod) { List other = new ArrayList<>(); other.add("x"); @@ -121,7 +122,7 @@ void testReplaceAll( void testSubList(@Modifiable List mod, @Unmodifiable List unmod) { List sub = mod.subList(0, mod.size()); - sub.clear(); // OK because sub is modifiable + sub.addAll(List.of("test")); // OK because sub is modifiable // :: error: [assignment] @Modifiable List sub2 = unmod.subList(0, unmod.size()); diff --git a/checker/tests/modifiability/CollectionModifiableTest.java b/checker/tests/modifiability/CollectionModifiableTest.java index 89ae8faab908..0fe19540dcf4 100644 --- a/checker/tests/modifiability/CollectionModifiableTest.java +++ b/checker/tests/modifiability/CollectionModifiableTest.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Spliterator; import java.util.stream.Stream; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -70,7 +71,7 @@ void testFactoryAssignments() { } void testAnnotatedLocalsFromParams( - @Modifiable Collection mod, + @Modifiable @IteratorPolyMod Collection mod, @Unmodifiable Collection unmod, @MaybeModifiable Collection unknown) { mod.add("p"); diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index bbfb0402a693..c599cc1fbbe1 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -55,11 +55,11 @@ void testBinarySearch() { void testCheckedWrappers() { @Modifiable Collection c = new ArrayList<>(); @Modifiable Collection checkedC = Collections.checkedCollection(c, String.class); - checkedC.add("a"); + checkedC.addAll(List.of("a")); @Modifiable List l = new ArrayList<>(); @Modifiable List checkedL = Collections.checkedList(l, String.class); - checkedL.add("a"); + checkedL.addAll(List.of("a")); @Modifiable Map m = new HashMap<>(); @Modifiable Map checkedM = Collections.checkedMap(m, String.class, String.class); @@ -253,11 +253,11 @@ void testSynchronizedWrappers() { // Synchronized wrappers pass through modifiability @Modifiable Collection c = new ArrayList<>(); @Modifiable Collection syncC = Collections.synchronizedCollection(c); - syncC.add("a"); + syncC.addAll(List.of("a")); @Modifiable List l = new ArrayList<>(); @Modifiable List syncL = Collections.synchronizedList(l); - syncL.add("a"); + syncL.addAll(List.of("a")); } void testUnmodifiableWrappers() { From adcb84259031966e7a227212b87e289a7eddafe1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 3 Jun 2026 22:35:35 -0700 Subject: [PATCH 218/308] combined shouldCheckUnmodifiableParamLocation and shouldCheckCustomModifiabilityAnnotation --- .../modifiability/ModifiabilityVisitor.java | 28 ++++++------------- .../modifiability/replace/ReplaceVisitor.java | 11 ++------ .../modifiability/seqgrow/SeqGrowVisitor.java | 11 ++------ .../modifiability/shrink/ShrinkVisitor.java | 11 ++------ 4 files changed, 14 insertions(+), 47 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 707c4cfbc261..4c0c641a7853 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -81,7 +81,7 @@ public ModifiabilityVisitor(BaseTypeChecker checker) { @Override public void processClassTree(ClassTree classTree) { super.processClassTree(classTree); - if (shouldCheckCustomModifiabilityAnnotation()) { + if (shouldCheckModifiabilityAnnotationValidity()) { TypeElement typeElement = TreeUtils.elementFromDeclaration(classTree); if (typeElement != null && isCollectionFromSourceCode(typeElement) @@ -115,7 +115,7 @@ public void processMethodTree(String className, MethodTree tree) { @Override public Void visitAnnotation(AnnotationTree tree, Void p) { - if (shouldCheckUnmodifiableParamLocation() && isUnmodifiableParamAnnotation(tree)) { + if (shouldCheckModifiabilityAnnotationValidity() && isUnmodifiableParamAnnotation(tree)) { if (allowedUnmodifiableParamAnnotations.isEmpty() || !allowedUnmodifiableParamAnnotations.peek().contains(tree)) { checker.reportError(tree, "unmodparam.location"); @@ -182,27 +182,15 @@ protected boolean checkOverride( } /** - * Returns true if this checker should report {@code @UnmodifiableParam} location errors. + * Returns true if this checker should report diagnostics about modifiability annotations whose + * validity is independent of a particular capability hierarchy. * - *

The default is {@code true}. Shrink and Replace override this to avoid tripling diagnostics - * when running under the aggregate {@link ModifiabilityChecker}. + *

The default is {@code true}. SeqGrow, Shrink, and Replace override this to avoid repeating + * diagnostics when running under the aggregate {@link ModifiabilityChecker}. * - * @return true if this visitor should report {@code @UnmodifiableParam} location errors + * @return true if this visitor should report shared modifiability annotation diagnostics */ - protected boolean shouldCheckUnmodifiableParamLocation() { - return true; - } - - /** - * Returns true if this checker should report warnings for source-defined Collection, Map, and - * Iterator subtypes whose modifiability annotations are trusted but not verified. - * - *

The default is {@code true}. Shrink and Replace override this to avoid tripling diagnostics - * when running under the aggregate {@link ModifiabilityChecker}. - * - * @return true if this visitor should report custom modifiability annotation warnings - */ - protected boolean shouldCheckCustomModifiabilityAnnotation() { + protected boolean shouldCheckModifiabilityAnnotationValidity() { return true; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 2b9e44a0db66..1f443814c56e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -16,15 +16,8 @@ public ReplaceVisitor(BaseTypeChecker checker) { } @Override - protected boolean shouldCheckUnmodifiableParamLocation() { - // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location - // reporting. - return checker.getParentChecker() == null; - } - - @Override - protected boolean shouldCheckCustomModifiabilityAnnotation() { - // When running under ModifiabilityChecker, GrowChecker handles custom type warnings. + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. return checker.getParentChecker() == null; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index 31ec6c49d530..bc7aade80dfc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -16,15 +16,8 @@ public SeqGrowVisitor(BaseTypeChecker checker) { } @Override - protected boolean shouldCheckUnmodifiableParamLocation() { - // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location - // reporting. - return checker.getParentChecker() == null; - } - - @Override - protected boolean shouldCheckCustomModifiabilityAnnotation() { - // When running under ModifiabilityChecker, GrowChecker handles custom type warnings. + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. return checker.getParentChecker() == null; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index a89b4256336a..0682d1138c33 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -16,15 +16,8 @@ public ShrinkVisitor(BaseTypeChecker checker) { } @Override - protected boolean shouldCheckUnmodifiableParamLocation() { - // When running under ModifiabilityChecker, GrowChecker handles @UnmodifiableParam location - // reporting. - return checker.getParentChecker() == null; - } - - @Override - protected boolean shouldCheckCustomModifiabilityAnnotation() { - // When running under ModifiabilityChecker, GrowChecker handles custom type warnings. + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. return checker.getParentChecker() == null; } } From d53e839526a6529b388ba8d21f0eb6a4381b6e25 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 4 Jun 2026 09:12:20 -0700 Subject: [PATCH 219/308] First pass. --- docs/manual/modifiability-checker.tex | 48 ++++++++++++--------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index c8646af895f6..945c70959307 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -2,25 +2,21 @@ \newcommand{\UnsupportedOperationException}{\} -In Java, an \emph{optional} method is one that a class may or may not -support. (Optional methods are unrelated to the \ type.) -An example is +In the Java Collections Framework, an \emph{optional} method is one that a class may or may not support. +If a method is unsupported, calling it throws \UnsupportedOperationException . +The Modifiability Checker verifies that optional methods related to modifying collections are only +called if they are supported, thus eliminating the possibility of an \UnsupportedOperationException at runtime. +(It is possible to write a checker that warns about other unsupported +methods, but the Modifiability Checker focuses on those issued when modifying collections.) + +%% (Optional methods are unrelated to the \ type.) [[ I would just remove this sentence it is distracting.]] +An example of an optional method is \sunjavadoc{java.base/java/util/Collection.html\#add(E)}{add}. Some subclasses of \ support \, and others (such as the result of calling \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableCollection(java.util.Collection)}{Collections.unmodifiableCollection}) do not support \. -Calling an unsupported method throws \UnsupportedOperationException. Thus, -whenever a client calls an optional method, there is a possibility of an -exception. The Modifiability Checker eliminates that possibility. - -To run the Modifiability Checker: - -\begin{Verbatim} -javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java -\end{Verbatim} - If the Modifiability Checker issues no warning, then your program is guaranteed not to throw \UnsupportedOperationException\ due to invoking an optional method (such as @@ -28,12 +24,13 @@ \sunjavadoc{java.base/java/util/Collection.html\#remove(java.lang.Object)}{remove}) on an unmodifiable collection. (For brevity, this chapter generally uses -just ``collection'' to mean ``collection, map, or iterator''.) +just ``collection'' to mean ``collection, map, or iterator''.). -It is possible to write a checker that warns about other unsupported -methods (beyond those in the Java Collections Framework), but the -Modifiability Checker does not do so. +To run the Modifiability Checker: +\begin{Verbatim} +javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java +\end{Verbatim} \sectionAndLabel{Unmodifiable collections}{modifiability-unmodifiable-collections} @@ -60,22 +57,19 @@ \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableMap(java.util.Map)}{\code{Collections.unmodifiableMap()}} \end{itemize} -A \emph{modifiable collection} is one on which mutating methods can be called -without throwing \UnsupportedOperationException. Non-optional methods, such -as \code{size}, \code{get}, \code{contains}, \code{isEmpty}, and \code{toArray}, may be -called on any collection, whether modifiable or unmodifiable. +A \emph{modifiable collection} supports calling at least one optional mutating method. In genernal, +classes support groups of methods which correspond to four different ways a collection can be modified. +A collection may support modifying a collection through one or more of the following capabilities. -Some collections and maps may be modified in some ways but not in others. -A collection or map can be modified in four different ways: -\textbf{Grow}, \textbf{SeqGrow}, \textbf{Shrink}, and \textbf{Replace}. +\textbf{Grow}, \textbf{Sequential Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements - (\, \, \, \ldots). - \item \textbf{SeqGrow}: A sequenced collection can grow by adding new + (\, \, \, \ldots) at arbitrary locations. + \item \textbf{Sequential Grow}: A sequenced collection can grow by adding new elements at the beginning or end (\, \, \, \, \, \ldots), and a sequenced map can insert or replace mappings at the beginning or end (\, - \). + \). (This is shorted to SeqGrow in the rest of the manual.) \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing From 3e6a8751a997351ebac641eeedd5243339d770c0 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 4 Jun 2026 11:04:34 -0700 Subject: [PATCH 220/308] A comment. --- docs/manual/modifiability-checker.tex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 945c70959307..52e9f2ec5dab 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -80,6 +80,9 @@ \noindent Some collections can also be modified through their iterator; see Section~\ref{modifiability-iterator-behavior}. +%% TODO: For each category what methods may or may not be called? I would guess this is in the +% annotated JDK, but where do I find this information? + \sectionAndLabel{Modifiability annotations}{modifiability-annotations} @@ -355,7 +358,7 @@ \begin{description} \item[\refqualclass{checker/modifiability/qual}{PreservesModifiability}] -When written on a one-argument method declaration, it indicates that the +When written on a non-void, one-argument method declaration, it indicates that the method preserves positive modifiability capabilities from its argument to its return value. If the argument is \<@Growable>, \<@Shrinkable>, \<@Replaceable>, \<@SeqGrowable>, or \<@IteratorPolyMod>, then the Modifiability Checker From 25fae0c3cddefe563e7e8ec02f04d7c48bc5def2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 13:40:40 -0700 Subject: [PATCH 221/308] refine aliasing in the manual. --- docs/manual/modifiability-checker.tex | 86 ++++++++++++++++++--------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 52e9f2ec5dab..ef13ff1390a5 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -375,19 +375,54 @@ \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, seq-grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} -%% TODO add explicit examples TODO, also in the Javadoc TODO. - Ordinarily, the declaration \<@Modifiable MyClass x;> is equivalent to \<@Growable @SeqGrowable @Shrinkable @Replaceable MyClass x;>, and \<@Unmodifiable MyClass x;> is equivalent to -\<@Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable MyClass x;>. However, there are -exceptions: +\<@Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable MyClass x;>. + +However, some static types lack methods for one or more mutation +capabilities. In such cases, the Modifiability Checker weakens the +structurally unavailable component to the corresponding top qualifier +rather than to the negative qualifier. For example, \ has +\ but not \ or \, so \<@Modifiable Iterator> carries +\<@Shrinkable>, but its grow and replace components are weakened to +\<@MaybeGrowable> and \<@MaybeReplaceable>. + +The following rules define this weakening. The first qualifier in each +capability row is used for \<@Modifiable>; the second is used for +\<@Unmodifiable>. If the weakening condition holds, both aliases use the +corresponding \<@Maybe*> qualifier instead. + +\begin{tabular}{ll} +Capability & Weakening condition \\ +\/\ & + \ or an \ that is not a \ \\ +\/\ & + not a \, \, or \ \\ +\/\ & + \ \\ +\/\ & + exact \, any \, any non-\ \, + or an \ that is not a \ \\ +\end{tabular} + +These rules describe alias expansion, not the full specification of every JDK +type. The JDK annotations may still give a more precise capability for a +particular declaration. For instance, \ is a +\ and \ is a \, so their +seq-grow component is structurally relevant and is not weakened to +\<@MaybeSeqGrowable>. Their JDK declarations are annotated +\<@SeqUngrowable>, because their positional insertion methods such as +\, \, \, and \ always throw +\UnsupportedOperationException. + + The following are representative canonical expansions. -%% TODO add more examples, exhaustively if possible. \begin{tabular}{rlcrrrrl} -% \<@Modifiable X> & = & \<@Growable> & \<@SeqGrowable> & \<@Shrinkable> & \<@Replaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ \<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ \<@Modifiable> & \ & = & \<@Growable> & \<@SeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ @@ -400,35 +435,28 @@ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ \<@Modifiable> & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ \<@Unmodifiable> & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ +\<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@Replaceable> & \ \\ +\<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@Unreplaceable> & \ \\ +\ & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@MaybeShrinkable> & \<@Replaceable> & \ \\ +\ & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@MaybeShrinkable> & \<@Unreplaceable> & \ \\ \end{tabular} -When a programmer writes \<@Modifiable> or \<@Unmodifiable> on a type that -has no methods for a given mutation, the Modifiability Checker uses the top -qualifier. -For example, \sunjavadoc{java.base/java/util/Queue.html}{\code{Queue}} has -\ and \ methods but no replacement methods. -It is not a subtype of \ or \, so it also has no -\, \, \, or \ methods. By contrast, \ is a -\, so its seq-grow capability is relevant; the JDK -annotations mark it \<@SeqUngrowable> because its \ and -\ methods always throw \UnsupportedOperationException. Likewise, -\ is a \, so the JDK annotations mark it -\<@SeqUngrowable> because its \ and \ methods always throw -\UnsupportedOperationException. - +Using a top qualifier for a structurally unavailable capability is important. +For example, the static type \ does not declare \ or \, but +\ is a subtype of \ and does declare those methods. +Therefore \<@Modifiable Iterator> should not mean that every possible runtime +iterator is incapable of grow or replace; it means only that those capabilities +are not known from the static type \. -Although no object of class \ supports replace operations, -the type -\<@Replaceable Queue> still makes sense, because some objects of type -\ do support replace operations. -For example, \<@Replaceable Queue> is the type of -\ in \. -This behavior ensures that code like this type-checks: +Similarly, exact \ does not declare replacement methods, but a +\ reference can point to a \, which does support +replacement operations. This behavior ensures that code like this +type-checks: \begin{Verbatim} -@Growable LinkedList list = ...; +@Replaceable LinkedList list = ...; Queue q = list; -@Growable LinkedList list2 = (Queue) q; // still @Growable +@Replaceable LinkedList list2 = (LinkedList) q; // still @Replaceable \end{Verbatim} From 67a4943c66dcb24b44e714280feff264c5a16ed0 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 13:58:55 -0700 Subject: [PATCH 222/308] clean up typos and comments --- .../checker/modifiability/qual/MaybeModifiable.java | 10 ++++++---- .../modifiability/qual/PreservesModifiability.java | 9 +++++---- .../checker/modifiability/qual/Unmodifiable.java | 7 ++++--- .../ModifiabilityAnnotatedTypeFactory.java | 2 +- .../modifiability/ModifiabilityBaseChecker.java | 5 +++-- .../checker/modifiability/ModifiabilityVisitor.java | 9 +++++---- checker/tests/modifiability/SetTest.java | 2 +- docs/manual/modifiability-checker.tex | 6 +++++- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java index c4b7e274efda..eafb5cce1344 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.java @@ -7,12 +7,14 @@ import java.lang.annotation.Target; /** - * Convenience alias meaning {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable}. Represents - * an unknown or arbitrary modifiability capability; the checker cannot determine whether the - * collection is growable, shrinkable, or replaceable. + * Convenience alias meaning + * {@code @MaybeGrowable @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable}. Represents an + * unknown or arbitrary modifiability capability; the checker cannot determine whether the + * collection is growable, sequenced-growable, shrinkable, or replaceable. * *

This annotation is not part of the type hierarchy; the Modifiability Checker expands it to - * {@code @MaybeGrowable @MaybeShrinkable @MaybeReplaceable} on each annotated type. + * {@code @MaybeGrowable @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable} on each annotated + * type. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java index 5bbcc4f6e677..8c7264ede5fc 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -10,10 +10,11 @@ * The annotated method preserves modifiability capabilities from its first argument to its return * value. * - *

For example, if the first argument is {@code @Modifiable} or {@code @IteratorPolyMod}, then - * the return type is also {@code @Modifiable} or {@code @IteratorPolyMod}. If the first argument is - * anything other than {@code @Modifiable} or {@code @IteratorPolyMod}, then the return type is the - * top {@code @MaybeModifiable}. + *

For example, if the first argument has a positive modifiability capability, such as + * {@code @Growable}, {@code @SeqGrowable}, {@code @Shrinkable}, or {@code @Replaceable}, then the + * return type has that same capability. If the first argument is {@code @IteratorPolyMod}, then the + * return type is also {@code @IteratorPolyMod}. If the first argument has any other qualifier in a + * capability hierarchy, then the return type is the top qualifier in that hierarchy. * *

If the annotated method has no parameters, then this annotation has no effect on the method. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java index 2e233b798c11..9c4e5fb1dba6 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.java @@ -7,13 +7,14 @@ import java.lang.annotation.Target; /** - * Convenience alias usually meaning {@code @Ungrowable @Unshrinkable @Unreplaceable}. Calling a - * mutating operation (growing, shrinking, or replacing) may throw {@link + * Convenience alias usually meaning + * {@code @Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable}. Calling a mutating operation + * (growing, sequenced-growing, shrinking, or replacing) will throw {@link * UnsupportedOperationException}. * *

As an exception, {@code @Unmodifiable} means {@code @Maybe*} if the type does not support a * given category of operation; for example, {@code @Unmodifiable Iterator} means - * {@code @MaybeGrowable @Unshrinkable @MaybeReplaceable Iterator}. + * {@code @MaybeGrowable @MaybeSeqGrowable @Unshrinkable @MaybeReplaceable Iterator}. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index c4b0242ae87a..0fa7083d5075 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -43,7 +43,7 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { * be imprecise to always use {@code @Maybe*}, because passing a positive type collection * guarantees a positive return type. * - *

This method is called by all three sub-checkers. + *

This method is called by all four sub-checkers. * * @param tree an invocation of a {@code @PreservesModifiability} method * @param methodType the annotated executable type of the invoked method diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index ff37c180ae40..c0aa8c2fcf22 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -8,8 +8,9 @@ *

This class exists so {@link * org.checkerframework.framework.source.SourceChecker#getMessagesProperties()} finds the shared * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The Grow, - * Shrink, and Replace checkers all report diagnostics whose message keys are defined there; without - * this common superclass, those keys would need to be duplicated in each sub-checker's package. + * SeqGrow, Shrink, and Replace checkers all report diagnostics whose message keys are defined + * there; without this common superclass, those keys would need to be duplicated in each + * sub-checker's package. */ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 4c0c641a7853..fcff0176c039 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -25,9 +25,9 @@ import org.checkerframework.javacutil.TypesUtils; /** - * Base visitor for the modifiability sub-checkers (Grow, Shrink, Replace). + * Base visitor for the modifiability sub-checkers (Grow, SeqGrow, Shrink, Replace). * - *

This class contains logic shared across all three sub-checkers: + *

This class contains logic shared across all four sub-checkers: * *

    *
  • Suppressing the "constructor result must be TOP" check, since collection constructors may @@ -221,7 +221,7 @@ private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { * constructor */ private boolean hasExplicitWarningModifiabilityAnnotation(ClassTree classTree) { - // write explicit modifianility annotations on the class + // write explicit modifiability annotations on the class if (hasWarningModifiabilityAnnotation(classTree.getModifiers().getAnnotations())) { return true; } @@ -241,7 +241,8 @@ && hasWarningModifiabilityAnnotation(methodTree.getModifiers().getAnnotations()) * Returns true if the list {@code annotations} contains any explicit non-maybe modifiability * qualifier. * - *

    Iterator throw the list of annotations and call isWarningModifiablityAnnotation(annotation) + *

    Iterator through the list of annotations and call + * isWarningModifiablityAnnotation(annotation) * * @param annotations annotation trees to inspect * @return true if one annotation should trigger an unverified modifiability warning diff --git a/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java index 4a3e384eb631..9ddea621efb1 100644 --- a/checker/tests/modifiability/SetTest.java +++ b/checker/tests/modifiability/SetTest.java @@ -105,7 +105,7 @@ void testEntryAssignment( Map.@MaybeGrowable @MaybeShrinkable @Replaceable Entry r3 = modifiable; // Explicit Grow/Shrink annotations are preserved on Map.Entry. // :: error: [assignment] - Map.@Growable @Replaceable Entry gr1 = replaceable; // OK: same effective type + Map.@Growable @Replaceable Entry gr1 = replaceable; // :: error: [assignment] Map.@Shrinkable @Replaceable Entry sr1 = replaceable; Map.@Modifiable Entry m1 = replaceable; diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index ef13ff1390a5..26b9ae8c6033 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -465,7 +465,7 @@ By default, the signature of \ is \begin{Verbatim} -@Unshrinkable Iterator iterator() +@MaybeShrinkable Iterator iterator() \end{Verbatim} However, for some subclasses of \, the signature of @@ -489,6 +489,10 @@ @Unshrinkable List list2 = ...; @Unshrinkable Iterator it2 = list2.iterator(); it2.remove(); // Error: it2 is @Unshrinkable + + @Shrinkable List list3 = ...; + @MaybeShrinkable Iterator it3 = list3.iterator(); + it3.remove(); // Error: list3 does not have @IteratorPolyMod \end{Verbatim} From 340172fb1a53adc4a3a6fb2ef43ce1adaeeac480 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 14:20:34 -0700 Subject: [PATCH 223/308] fixed more typo Co-authored-by: Copilot --- .../checker/modifiability/qual/MaybeIteratorPolyMod.java | 2 +- .../checker/modifiability/qual/PolyModifiable.java | 2 +- docs/manual/modifiability-checker.tex | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java index f113fdc671f6..d7a6ea8b5a49 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java @@ -9,7 +9,7 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * For the annotated collection, the return type of {@code iterator()} is {@code @Unshrinkable}. + * For the annotated collection, the return type of {@code iterator()} is {@code @MaybeShrinkable}. * *

    This is the default qualifier for unannotated types. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 83724f7d4be5..af85d719cf53 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -9,7 +9,7 @@ * for all four modifiability hierarchies. * *

    Write {@code @PolyModifiable} on methods that preserve or transfer modifiability, such as - * {@code List.subList()} and {@code stream()}. + * {@code List.subList()}. * *

    For example: * diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 26b9ae8c6033..6b1695079464 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -57,7 +57,7 @@ \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableMap(java.util.Map)}{\code{Collections.unmodifiableMap()}} \end{itemize} -A \emph{modifiable collection} supports calling at least one optional mutating method. In genernal, +A \emph{modifiable collection} supports calling at least one optional mutating method. In general, classes support groups of methods which correspond to four different ways a collection can be modified. A collection may support modifying a collection through one or more of the following capabilities. @@ -69,7 +69,7 @@ elements at the beginning or end (\, \, \, \, \, \ldots), and a sequenced map can insert or replace mappings at the beginning or end (\, - \). (This is shorted to SeqGrow in the rest of the manual.) + \). (This is shortened to SeqGrow in the rest of the manual.) \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing From 8f37570c077d6132579161aba29a0f2ffb4084b7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 14:34:25 -0700 Subject: [PATCH 224/308] edit @PreservesModifiabilityReturn type to detect void method. --- .../modifiability/qual/PreservesModifiability.java | 3 ++- .../ModifiabilityAnnotatedTypeFactory.java | 11 +++++++---- .../modifiability/PreservesModifiabilityTest.java | 7 +++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java index 8c7264ede5fc..c5f414ad7693 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -16,7 +16,8 @@ * return type is also {@code @IteratorPolyMod}. If the first argument has any other qualifier in a * capability hierarchy, then the return type is the top qualifier in that hierarchy. * - *

    If the annotated method has no parameters, then this annotation has no effect on the method. + *

    If the annotated method has no parameters or returns {@code void}, then this annotation has no + * effect on the method. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 0fa7083d5075..518ab1a50f2b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -3,6 +3,7 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeKind; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -30,7 +31,7 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { /** * Refines the return type of a {@code @PreservesModifiability} method. * - *

    If the method has no parameters, then this annotation has no effect. + *

    If the method has no parameters or returns {@code void}, then this annotation has no effect. * *

    Otherwise, if the first argument is positive (i.e.{@code @Shrinkable}), then the return type * is also positive. If the first argument is {@code @IteratorPolyMod}, then the return type is @@ -50,15 +51,17 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { */ protected void refinePreservesModifiabilityReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { - if (tree.getArguments().isEmpty()) { + AnnotatedTypeMirror returnType = methodType.getReturnType(); + if (tree.getArguments().isEmpty() + || returnType.getUnderlyingType().getKind() == TypeKind.VOID) { return; } AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(positiveCapability())) { - methodType.getReturnType().replaceAnnotation(positiveCapability()); + returnType.replaceAnnotation(positiveCapability()); } if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - methodType.getReturnType().replaceAnnotation(ITERATOR_PRESERVE_REMOVE); + returnType.replaceAnnotation(ITERATOR_PRESERVE_REMOVE); } } diff --git a/checker/tests/modifiability/PreservesModifiabilityTest.java b/checker/tests/modifiability/PreservesModifiabilityTest.java index 6f29c934fc8d..4e1c6ecf770d 100644 --- a/checker/tests/modifiability/PreservesModifiabilityTest.java +++ b/checker/tests/modifiability/PreservesModifiabilityTest.java @@ -19,6 +19,9 @@ static List unannotated(Collection values) { return new ArrayList<>(values); } + @PreservesModifiability + static void annotatedVoid(Collection values) {} + void preservesCapabilities( @Growable List growable, @Shrinkable List shrinkable, @@ -49,4 +52,8 @@ void unannotatedDoesNotPreserve( // :: error: [assignment] @Modifiable List m = unannotated(modifiable); } + + void voidAnnotatedMethodHasNoEffect(@Growable List growable) { + annotatedVoid(growable); + } } From e33375bbe676f59badadbbf58a6118bac13fa188 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 15:21:32 -0700 Subject: [PATCH 225/308] fixed more documentation. Co-authored-by: Copilot --- .../modifiability/qual/IteratorPolyMod.java | 7 +++++-- .../modifiability/ModifiabilityVisitor.java | 2 +- .../CustomModifiabilityAnnotationWarning.java | 20 +++++++++++++------ .../modifiability/PolyModifiableTest.java | 5 +++-- docs/manual/modifiability-checker.tex | 2 +- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index eab5c7afc93e..0fd6d04fcfd4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -13,8 +13,11 @@ * *

    For example, if collection {@code c} has type {@code @Shrinkable}, then {@code c.iterator()} * also has type {@code @Shrinkable}. If list {@code l} has type {@code @Replaceable}, then {@code - * l.listIterator()} also has type {@code @Replaceable}. For any collection whose type is - * {@code @MaybeIteratorPolyMod}, its iterator is always {@code @MaybeShrinkable}. + * l.listIterator()} also has type {@code @Replaceable}. + * + *

    If the collection itself is {@code @Unmodifiable}, then its iterator is {@code @Unshrinkable}. + * For any collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is + * {@code @MaybeShrinkable}. * *

    This annotation is used in GrowChecker, ShrinkChecker, and ReplaceChecker. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index fcff0176c039..bd71716cf441 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -126,7 +126,7 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { /** * Returns the positive qualifier for this checker's modifiability hierarchy, such as - * {@code @Growable}, {@code @Shrinkable}, or {@code @Replaceable}. + * {@code @Growable}, {@code @SeqGrowable}, {@code @Shrinkable}, or {@code @Replaceable}. * * @return this checker's positive capability qualifier */ diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index 64effead0dd1..67bab3a237bd 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -123,8 +123,7 @@ public int size() { } } - @SuppressWarnings("modifiability:annotation.unverified") - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] static @Modifiable class SuppressedList extends AbstractList { @Override public String get(int index) { @@ -135,13 +134,22 @@ public String get(int index) { public int size() { return 1; } + + @Override + // Only ShrinkChecker and ReplaceChecker report this receiver override. + // The class-level @Modifiable makes SuppressedList @SeqGrowable too, but + // SeqGrowChecker ignores @IteratorPolyMod. As a result, the receiver appears + // to match SuppressedList's declaration bound in the SeqGrow hierarchy, so + // BaseTypeVisitor's declaration-bound exception accepts the override. + // :: error: [override.receiver] + public boolean add(@IteratorPolyMod SuppressedList this, String e) { + return super.add(e); + } } - @SuppressWarnings("modifiability:annotation.unverified") - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] abstract static @Unmodifiable class SuppressedMap implements Map {} - @SuppressWarnings("modifiability:annotation.unverified") - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] abstract static @IteratorPolyMod class SuppressedIterator implements Iterator {} } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 939ec1ed4ae5..4967eaf1e56b 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -8,8 +8,9 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; /** - * Tests @PolyModifiable, which expands to @PolyGrowable @PolyShrinkable @PolyReplaceable and thus - * preserves all three capabilities independently. + * Tests @PolyModifiable, which expands + * to @PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable and thus preserves all four + * capabilities independently. */ public class PolyModifiableTest { diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 6b1695079464..863214fecbe0 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -370,7 +370,7 @@ This is useful for methods that may return their argument, or may return a new modifiable collection. -If put on a method with no parameters, then it has no effect. +If put on a void method or a method with no parameters, then it has no effect. \end{description} \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, seq-grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} From ddda816ff10791ee75374575eed9887b8dc0f060 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 15:34:16 -0700 Subject: [PATCH 226/308] expand HierarchyTest and PolyModifiableTest to include SeqGrow. --- .../tests/modifiability/HierarchyTest.java | 104 +++++++++++++++++- .../modifiability/PolyModifiableTest.java | 69 +++++++++--- 2 files changed, 155 insertions(+), 18 deletions(-) diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index b005b551464e..c0094dae5f41 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -1,13 +1,18 @@ +import java.util.Deque; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; +import org.checkerframework.checker.modifiability.qual.BottomSeqGrowable; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -15,7 +20,7 @@ import org.checkerframework.checker.modifiability.qual.Unshrinkable; /** - * Tests the three independent 4-element lattices of the Modifiability Checker. + * Tests the four independent 4-element capability lattices of the Modifiability Checker. * *

    Each hierarchy is: Maybe* (top/default), two incomparable siblings (*able and Un*able), and * Bottom* (bottom). @@ -75,6 +80,34 @@ void testShrink( @Unshrinkable Object us3 = u; } + void testSeqGrow( + @SeqGrowable Object sg, + @SeqUngrowable Object usg, + @MaybeSeqGrowable Object u, + @BottomSeqGrowable Object b) { + @MaybeSeqGrowable Object u1 = sg; + @MaybeSeqGrowable Object u2 = usg; + @MaybeSeqGrowable Object u3 = b; + + @SeqGrowable Object sg1 = b; + @SeqUngrowable Object usg1 = b; + + // :: error: [assignment] + @SeqGrowable Object sg2 = usg; + // :: error: [assignment] + @SeqUngrowable Object usg2 = sg; + // :: error: [assignment] + @SeqGrowable Object sg3 = u; + // :: error: [assignment] + @SeqUngrowable Object usg3 = u; + // :: error: [assignment] + @BottomSeqGrowable Object b1 = sg; + // :: error: [assignment] + @BottomSeqGrowable Object b2 = usg; + // :: error: [assignment] + @BottomSeqGrowable Object b3 = u; + } + void testReplace( @Replaceable Object r, @Unreplaceable Object ur, @@ -97,6 +130,46 @@ void testReplace( @Unreplaceable Object ur3 = u; } + void testSeqGrowCombinations( + @Growable @SeqGrowable Object gsg, + @Growable Object g, + @SeqGrowable Object sg, + @Ungrowable @SeqUngrowable Object none, + @MaybeGrowable @MaybeSeqGrowable Object unknown) { + + @Growable Object gv1 = gsg; + @SeqGrowable Object sgv1 = gsg; + + @Growable Object gv2 = g; + // :: error: [assignment] + @SeqGrowable Object sgv2 = g; + + // :: error: [assignment] + @Growable Object gv3 = sg; + @SeqGrowable Object sgv3 = sg; + + // :: error: [assignment] + @Growable Object gv4 = none; + // :: error: [assignment] + @SeqGrowable Object sgv4 = none; + + // :: error: [assignment] + @Growable Object gv5 = unknown; + // :: error: [assignment] + @SeqGrowable Object sgv5 = unknown; + + @MaybeGrowable @MaybeSeqGrowable + Object tv1 = gsg; + @MaybeGrowable @MaybeSeqGrowable + Object tv2 = g; + @MaybeGrowable @MaybeSeqGrowable + Object tv3 = sg; + @MaybeGrowable @MaybeSeqGrowable + Object tv4 = none; + @MaybeGrowable @MaybeSeqGrowable + Object tv5 = unknown; + } + void testCombinations( @Growable @Shrinkable @Replaceable Object gsr, @Growable @Shrinkable Object gs, @@ -173,6 +246,7 @@ void testAliases( @Unreplaceable Object ur1 = unmod; @MaybeGrowable Object ug2 = unknown; + @MaybeSeqGrowable Object usg2 = unknown; @MaybeShrinkable Object us2 = unknown; @MaybeReplaceable Object ur2 = unknown; @@ -185,7 +259,35 @@ void testAliases( @Unmodifiable Object unmod2 = unknown; // :: error: [assignment] @Growable Object g2 = unmod; + // @Object cannot structurally support sequenced-grow operations, so whole-modifiability aliases + // use @MaybeSeqGrowable for their SeqGrow component. + @MaybeSeqGrowable Object maybeSeqGrowable1 = mod; + @MaybeSeqGrowable Object maybeSeqGrowable2 = unmod; + // :: error: [assignment] + @SeqGrowable Object sg1 = mod; + // :: error: [assignment] + @SeqUngrowable Object usg1 = unmod; // :: error: [assignment] @Modifiable Object mod1 = unmod; } + + void testAliasesOnSeqGrowableType( + @Modifiable Deque mod, + @Unmodifiable Deque unmod, + @MaybeModifiable Deque unknown) { + + @SeqGrowable Deque sg1 = mod; + @SeqUngrowable Deque usg1 = unmod; + @MaybeSeqGrowable Deque msg1 = unknown; + + @MaybeSeqGrowable Deque msg2 = mod; + @MaybeSeqGrowable Deque msg3 = unmod; + + // :: error: [assignment] + @SeqUngrowable Deque usg2 = mod; + // :: error: [assignment] + @SeqGrowable Deque sg2 = unmod; + // :: error: [assignment] + @SeqGrowable Deque sg3 = unknown; + } } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 4967eaf1e56b..f4d152ce6ecd 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,9 +1,12 @@ +import java.util.Deque; import java.util.List; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; @@ -14,11 +17,16 @@ */ public class PolyModifiableTest { - /** A simple polymorphic identity method that preserves all three capabilities. */ + /** A simple polymorphic identity method that preserves all four capabilities. */ @PolyModifiable List identity(@PolyModifiable List x) { return x; } + /** A Deque-specific identity method for testing SeqGrow alias expansion on all supported JDKs. */ + @PolyModifiable Deque identityDeque(@PolyModifiable Deque x) { + return x; + } + void testPoly( @Modifiable List mod, @Growable @Shrinkable List gs, // Grow+Shrink; Replace = MaybeReplaceable (default) @@ -31,55 +39,55 @@ void testPoly( @Unmodifiable List unmod) { // ============================================================ - // Identity on @Modifiable (G+S+R) + // Identity on @Modifiable (G+Seq+S+R, on types that support all four capabilities) // ============================================================ - @Modifiable List m1 = identity(mod); // OK: poly resolves to G+S+R - @MaybeModifiable List m2 = identity(mod); // OK: G+S+R <: top in all + @Modifiable List m1 = identity(mod); // OK: poly resolves to @Modifiable + @MaybeModifiable List m2 = identity(mod); // OK: positive capabilities <: tops // ============================================================ - // Identity on @Growable @Shrinkable (G+S, R=MaybeReplaceable) + // Identity on @Growable @Shrinkable (G+S; Seq=MaybeSeqGrowable, R=MaybeReplaceable) // ============================================================ @Growable @Shrinkable List gs1 = identity(gs); // OK // :: error: [assignment] - @Modifiable List gs2 = identity(gs); // Error: R=Maybe !<: Replaceable - @Growable List gs3 = identity(gs); // OK: G+S+MaybeR <: G - @Shrinkable List gs4 = identity(gs); // OK: G+S+MaybeR <: S + @Modifiable List gs2 = identity(gs); // Error: Seq/R=Maybe !<: positive + @Growable List gs3 = identity(gs); // OK + @Shrinkable List gs4 = identity(gs); // OK // :: error: [assignment] @Replaceable List gs5 = identity(gs); // Error: MaybeR !<: Replaceable // ============================================================ - // Identity on @Growable @Replaceable (G+R, S=MaybeShrinkable) + // Identity on @Growable @Replaceable (G+R; Seq=MaybeSeqGrowable, S=MaybeShrinkable) // ============================================================ @Growable @Replaceable List gr1 = identity(gr); // OK // :: error: [assignment] - @Modifiable List gr2 = identity(gr); // Error: S=Maybe !<: Shrinkable + @Modifiable List gr2 = identity(gr); // Error: Seq/S=Maybe !<: positive @Growable List gr3 = identity(gr); // OK // :: error: [assignment] @Shrinkable List gr4 = identity(gr); // Error: S=Maybe @Replaceable List gr5 = identity(gr); // OK // ============================================================ - // Identity on @Shrinkable @Replaceable (S+R, G=MaybeGrowable) + // Identity on @Shrinkable @Replaceable (S+R; G=MaybeGrowable, Seq=MaybeSeqGrowable) // ============================================================ @Shrinkable @Replaceable List sr1 = identity(sr); // OK // :: error: [assignment] - @Modifiable List sr2 = identity(sr); // Error: G=Maybe !<: Growable + @Modifiable List sr2 = identity(sr); // Error: G/Seq=Maybe !<: positive // :: error: [assignment] @Growable List sr3 = identity(sr); // Error: G=Maybe @Shrinkable List sr4 = identity(sr); // OK @Replaceable List sr5 = identity(sr); // OK // ============================================================ - // Identity on @Growable (G only; S=Maybe, R=Maybe) + // Identity on @Growable (G only; Seq=Maybe, S=Maybe, R=Maybe) // ============================================================ @Growable List g1 = identity(g); // OK // :: error: [assignment] @Growable @Shrinkable List g2 = identity(g); // Error: S=Maybe // :: error: [assignment] - @Modifiable List g3 = identity(g); // Error: S=Maybe, R=Maybe + @Modifiable List g3 = identity(g); // Error: Seq/S/R=Maybe // ============================================================ - // Identity on @Shrinkable (S only; G=Maybe, R=Maybe) + // Identity on @Shrinkable (S only; G=Maybe, Seq=Maybe, R=Maybe) // ============================================================ @Shrinkable List s1 = identity(s); // OK // :: error: [assignment] @@ -88,7 +96,7 @@ void testPoly( @Modifiable List s3 = identity(s); // Error // ============================================================ - // Identity on @Replaceable (R only; G=Maybe, S=Maybe) + // Identity on @Replaceable (R only; G=Maybe, Seq=Maybe, S=Maybe) // ============================================================ @Replaceable List r1 = identity(r); // OK // :: error: [assignment] @@ -106,11 +114,38 @@ void testPoly( @Growable List u3 = identity(unknown); // Error // ============================================================ - // Identity on @Unmodifiable (Ungrowable+Unshrinkable+Unreplaceable) + // Identity on @Unmodifiable (negative capabilities, with structural weakening as needed) // ============================================================ @Unmodifiable List unmod1 = identity(unmod); // OK @MaybeModifiable List unmod2 = identity(unmod); // OK // :: error: [assignment] @Modifiable List unmod3 = identity(unmod); // Error } + + void testPolySeqGrowOnDeque( + @Modifiable Deque mod, + @SeqGrowable Deque seqGrow, + @SeqUngrowable Deque seqUngrow, + @MaybeModifiable Deque unknown, + @Unmodifiable Deque unmod) { + + @Modifiable Deque m1 = identityDeque(mod); // OK: preserves all four positives. + @SeqGrowable Deque m2 = identityDeque(mod); // OK: keeps SeqGrow. + + @SeqGrowable Deque sg1 = identityDeque(seqGrow); // OK + // :: error: [assignment] + @Modifiable Deque sg2 = identityDeque(seqGrow); // Error: only SeqGrow is known. + + @SeqUngrowable Deque usg1 = identityDeque(seqUngrow); // OK + // :: error: [assignment] + @SeqGrowable Deque usg2 = identityDeque(seqUngrow); // Error: negative SeqGrow. + + @SeqUngrowable Deque unmod1 = identityDeque(unmod); // OK + // :: error: [assignment] + @SeqGrowable Deque unmod2 = identityDeque(unmod); // Error: negative SeqGrow. + + @MaybeModifiable Deque unknown1 = identityDeque(unknown); // OK + // :: error: [assignment] + @SeqGrowable Deque unknown2 = identityDeque(unknown); // Error: SeqGrow is unknown. + } } From e72cfaa1f546eecd00cb6788add89a3964235740 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 15:42:32 -0700 Subject: [PATCH 227/308] update astub file. --- .../checker/modifiability/grow/javaparser.astub | 2 +- .../checker/modifiability/replace/javaparser.astub | 2 +- .../checker/modifiability/shrink/javaparser.astub | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub index dadc1d1762fe..2b5494331d72 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub @@ -4,7 +4,7 @@ import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; class BlockStmt { - public @Modifiable NodeList getStatements() {} + public @Modifiable @IteratorPolyMod NodeList getStatements() {} } package com.github.javaparser.ast; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub index dadc1d1762fe..2b5494331d72 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub @@ -4,7 +4,7 @@ import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; class BlockStmt { - public @Modifiable NodeList getStatements() {} + public @Modifiable @IteratorPolyMod NodeList getStatements() {} } package com.github.javaparser.ast; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub index dadc1d1762fe..2b5494331d72 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub @@ -4,7 +4,7 @@ import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; class BlockStmt { - public @Modifiable NodeList getStatements() {} + public @Modifiable @IteratorPolyMod NodeList getStatements() {} } package com.github.javaparser.ast; From ab29312013e8abfbc47904088059cf96a6229c82 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 4 Jun 2026 16:06:59 -0700 Subject: [PATCH 228/308] listed methods in each modifiability family. --- docs/manual/modifiability-checker.tex | 51 +++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 863214fecbe0..8b4b016561a8 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -64,7 +64,7 @@ \textbf{Grow}, \textbf{Sequential Grow}, \textbf{Shrink}, and \textbf{Replace}. \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements - (\, \, \, \ldots) at arbitrary locations. + (\, \, \, \ldots) at arbitrary locations. Not to be confused with sequential grow. \item \textbf{Sequential Grow}: A sequenced collection can grow by adding new elements at the beginning or end (\, \, \, \, \, \ldots), and a sequenced map can @@ -80,10 +80,55 @@ \noindent Some collections can also be modified through their iterator; see Section~\ref{modifiability-iterator-behavior}. -%% TODO: For each category what methods may or may not be called? I would guess this is in the -% annotated JDK, but where do I find this information? +The following table summarizes the main method families that may be called +for each capability. It is not a list of every overload. Read-only +methods such as \, \, \, \, \, and +\ do not require a modifiability capability. +\noindent +\begin{tabular}{p{0.22\linewidth}p{0.70\linewidth}} +Capability required & Method families \\ +\<@Growable> & + ordinary insertion: \, \, \, + \, \, \, \, + timed \, \, \, + \, \, \, + legacy \ insertion methods, and \ \\ +\<@SeqGrowable> & + front/back insertion or positioning (only applicable to sequenced collections and maps): + \, \, \, + \, \, \, \, + \, \, timed \, + timed \, \, \ \\ +\<@Shrinkable> & + removal: \, \, \, + \, \, \, \, \, + \, timed \, \, \, + \, \, \, occurrence-removal methods, + \, conditional \, \, + \, \, \, + and legacy \ removal methods \\ +\<@Replaceable> & + replacement without changing size: \, \, + \, \, \, + \, \, and mutating + \ algorithms such as \, \, \, + \, \, \, \, and \, plus + legacy \ replacement methods \\ +Combined capabilities & + \ and \ require \<@Growable @Replaceable>; + \ requires \<@Shrinkable @Replaceable>; + \ and \ require + \<@Growable @Shrinkable @Replaceable>; + \ requires a \<@Shrinkable> receiver and a + \<@Growable> target collection \\ +\end{tabular} +An overriding method must preserve positive modifiability receiver +requirements exactly. For example, if a superclass method has a +\<@Growable>, \<@SeqGrowable>, \<@Shrinkable>, or \<@Replaceable> receiver, +then the overriding method must require that same positive receiver +capability; it may not drop or weaken the requirement. \sectionAndLabel{Modifiability annotations}{modifiability-annotations} From 65da92e1a503ca114072c7a4e32e3871eba3c8e8 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 4 Jun 2026 16:33:32 -0700 Subject: [PATCH 229/308] Mike's edits --- docs/manual/modifiability-checker.tex | 111 +++++++++++++++----------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 26b9ae8c6033..04b0f69c9b07 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -3,9 +3,10 @@ \newcommand{\UnsupportedOperationException}{\} In the Java Collections Framework, an \emph{optional} method is one that a class may or may not support. -If a method is unsupported, calling it throws \UnsupportedOperationException . +If a method is unsupported, calling it throws \UnsupportedOperationException. The Modifiability Checker verifies that optional methods related to modifying collections are only -called if they are supported, thus eliminating the possibility of an \UnsupportedOperationException at runtime. +called if they are supported, thus eliminating the possibility of an +\UnsupportedOperationException\ at run time. (It is possible to write a checker that warns about other unsupported methods, but the Modifiability Checker focuses on those issued when modifying collections.) @@ -24,7 +25,7 @@ \sunjavadoc{java.base/java/util/Collection.html\#remove(java.lang.Object)}{remove}) on an unmodifiable collection. (For brevity, this chapter generally uses -just ``collection'' to mean ``collection, map, or iterator''.). +just ``collection'' to mean ``collection, map, or iterator''.) To run the Modifiability Checker: @@ -48,28 +49,35 @@ \sunjavadoc{java.base/java/util/Map.html\#of(K,V,K,V)}{\code{Map.of()}}, \sunjavadoc{java.base/java/util/Map.html\#copyOf(java.util.Map)}{\code{Map.copyOf()}} \item - \sunjavadoc{java/util/Collections.html\#emptyList()}{\code{Collections.emptyList()}} - \sunjavadoc{java/util/Collections.html\#emptySet()}{\code{Collections.emptySet()}} + \sunjavadoc{java/util/Collections.html\#emptyList()}{\code{Collections.emptyList()}}, + \sunjavadoc{java/util/Collections.html\#emptySet()}{\code{Collections.emptySet()}}, \sunjavadoc{java/util/Collections.html\#emptyMap()}{\code{Collections.emptyMap()}} \item - \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableList(java.util.List)}{\code{Collections.unmodifiableList()}} - \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableSet(java.util.Set)}{\code{Collections.unmodifiableSet()}} + \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableList(java.util.List)}{\code{Collections.unmodifiableList()}}, + \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableSet(java.util.Set)}{\code{Collections.unmodifiableSet()}}, \sunjavadoc{java.base/java/util/Collections.html\#unmodifiableMap(java.util.Map)}{\code{Collections.unmodifiableMap()}} \end{itemize} A \emph{modifiable collection} supports calling at least one optional mutating method. In genernal, classes support groups of methods which correspond to four different ways a collection can be modified. -A collection may support modifying a collection through one or more of the following capabilities. - +A collection may support modifying a collection through one or more of the capabilities \textbf{Grow}, \textbf{Sequential Grow}, \textbf{Shrink}, and \textbf{Replace}. + \begin{itemize} \item \textbf{Grow}: The collection can grow by adding new elements (\, \, \, \ldots) at arbitrary locations. - \item \textbf{Sequential Grow}: A sequenced collection can grow by adding new + \item \textbf{Sequential Grow}: The sequenced collection can grow by adding new elements at the beginning or end (\, \, - \, \, \, \ldots), and a sequenced map can - insert or replace mappings at the beginning or end (\, - \). (This is shorted to SeqGrow in the rest of the manual.) + \, \, \, \, + \, \ldots). + + The Sequencial Grow capability is needed for collections, such as + \sunjavadoc{java.base/java/util/TreeSet.html}{\code{TreeSet}}, that + support grow operations (like \ and \) but do not + support sequential grow operations (like \ and + \). The declaration of \ is annotated as \<@Growable @SeqUngrowable>. + + ``Sequential Grow'' is shorted to ``SeqGrow'' in the rest of the manual. \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing @@ -135,7 +143,7 @@ \item[\refqualclass{checker/modifiability/qual}{Modifiable}] Alias for \code{@Growable @SeqGrowable @Shrinkable @Replaceable} (but see -Section~\ref{modifiability-types-lacking-methods} for exceptions). Calling +Section \ref{modifiability-types-lacking-methods} for exceptions). Calling any mutating operation on this collection will not throw \UnsupportedOperationException. @@ -324,9 +332,14 @@ The return type's growability matches the growability of whichever formal parameter is annotated with \<@PolyGrowable>. +\item[\refqualclass{checker/modifiability/qual}{PolySeqGrowable}] +Polymorphic qualifier for the SeqGrow hierarchy. +The return type's seq-growability matches the seq-growability of +whichever formal parameter is annotated with \<@PolySeqGrowable>. + \item[\refqualclass{checker/modifiability/qual}{PolyShrinkable}] Polymorphic qualifier for the Shrink hierarchy. -The return type's Shrink qualifier matches the Shrink qualifier of +The return type's shrinkability qualifier matches the shrinkability of whichever formal parameter is annotated with \<@PolyShrinkable>. Useful for methods such as \code{Map.keySet()} that preserve shrinkability while returning an \<@Ungrowable> view. @@ -336,11 +349,6 @@ The return type's replaceability matches the replaceability of whichever formal parameter is annotated with \<@PolyReplaceable>. -\item[\refqualclass{checker/modifiability/qual}{PolySeqGrowable}] -Polymorphic qualifier for the SeqGrow hierarchy. -The return type's seq-growability matches the seq-growability of -whichever formal parameter is annotated with \<@PolySeqGrowable>. - \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable}. Preserves all four capabilities simultaneously. @@ -358,7 +366,8 @@ \begin{description} \item[\refqualclass{checker/modifiability/qual}{PreservesModifiability}] -When written on a non-void, one-argument method declaration, it indicates that the +Should only be written on a non-void, one-argument method declaration. +It indicates that the method preserves positive modifiability capabilities from its argument to its return value. If the argument is \<@Growable>, \<@Shrinkable>, \<@Replaceable>, \<@SeqGrowable>, or \<@IteratorPolyMod>, then the Modifiability Checker @@ -369,8 +378,6 @@ This is useful for methods that may return their argument, or may return a new modifiable collection. - -If put on a method with no parameters, then it has no effect. \end{description} \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, seq-grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} @@ -382,14 +389,40 @@ \<@Unmodifiable MyClass x;> is equivalent to \<@Ungrowable @SeqUngrowable @Unshrinkable @Unreplaceable MyClass x;>. -However, some static types lack methods for one or more mutation +However, some types lack methods for one or more mutation capabilities. In such cases, the Modifiability Checker weakens the -structurally unavailable component to the corresponding top qualifier -rather than to the negative qualifier. For example, \ has +structurally unavailable component to the corresponding top qualifier. +For example, \ has \ but not \ or \, so \<@Modifiable Iterator> carries \<@Shrinkable>, but its grow and replace components are weakened to \<@MaybeGrowable> and \<@MaybeReplaceable>. + +\subsectionAndLabel{Alternative design: use negative capability rather than top qualifier}{modifiability-modifyiable-must-be-top} + +An alternative design would use the + +Using a top qualifier for a structurally unavailable capability is important. +For example, the static type \ does not declare \ or \, but +\ is a subtype of \ and does declare those methods. +Therefore \<@Modifiable Iterator> should not mean that every possible runtime +iterator is incapable of grow or replace; it means only that those capabilities +are not known from the static type \. + +Similarly, exact \ does not declare replacement methods, but a +\ reference can point to a \, which does support +replacement operations. This behavior ensures that code like this +type-checks: + +\begin{Verbatim} +@Replaceable LinkedList list = ...; +Queue q = list; +@Replaceable LinkedList list2 = (LinkedList) q; // still @Replaceable +\end{Verbatim} + + +\subsectionAndLabel{Expansion rules for \<@Modifiable> and \<@Unmodifiable>}{modifiability-modifiable-expansion-rules} + The following rules define this weakening. The first qualifier in each capability row is used for \<@Modifiable>; the second is used for \<@Unmodifiable>. If the weakening condition holds, both aliases use the @@ -418,8 +451,13 @@ \, \, \, and \ always throw \UnsupportedOperationException. - The following are representative canonical expansions. +\subsectionAndLabel{Example expansions for \<@Modifiable> and \<@Unmodifiable>}{modifiability-modifiable-expansion-examples} + +The following are representative canonical expansions. + +\noindent +\begin{smaller} \begin{tabular}{rlcrrrrl} \<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ @@ -440,24 +478,7 @@ \ & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@MaybeShrinkable> & \<@Replaceable> & \ \\ \ & \ & = & \<@MaybeGrowable> & \<@MaybeSeqGrowable> & \<@MaybeShrinkable> & \<@Unreplaceable> & \ \\ \end{tabular} - -Using a top qualifier for a structurally unavailable capability is important. -For example, the static type \ does not declare \ or \, but -\ is a subtype of \ and does declare those methods. -Therefore \<@Modifiable Iterator> should not mean that every possible runtime -iterator is incapable of grow or replace; it means only that those capabilities -are not known from the static type \. - -Similarly, exact \ does not declare replacement methods, but a -\ reference can point to a \, which does support -replacement operations. This behavior ensures that code like this -type-checks: - -\begin{Verbatim} -@Replaceable LinkedList list = ...; -Queue q = list; -@Replaceable LinkedList list2 = (LinkedList) q; // still @Replaceable -\end{Verbatim} +\end{smaller} \sectionAndLabel{Behavior of the \ method}{modifiability-iterator-behavior} From 4b3a6000cf6395758fb360ca9660eac06e238d29 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 15:25:30 -0700 Subject: [PATCH 230/308] removed maybe qualifiers from AnnotatedTypeFactory becasue it is not used. --- .../modifiability/ModifiabilityAnnotatedTypeFactory.java | 7 ------- .../modifiability/grow/GrowAnnotatedTypeFactory.java | 5 ----- .../modifiability/replace/ReplaceAnnotatedTypeFactory.java | 5 ----- .../modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java | 5 ----- .../modifiability/shrink/ShrinkAnnotatedTypeFactory.java | 5 ----- 5 files changed, 27 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 518ab1a50f2b..1db4ae6e3118 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -116,13 +116,6 @@ protected void refineIteratorReturnType( } } - /** - * Returns the top/unknown qualifier. - * - * @return the top/unknown qualifier - */ - protected abstract AnnotationMirror maybeCapability(); - /** * Returns the positive capability qualifier. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 8d5c33df490c..99cc7b04075a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -78,11 +78,6 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } - @Override - protected AnnotationMirror maybeCapability() { - return MAYBE_GROWABLE; - } - @Override protected AnnotationMirror positiveCapability() { return GROWABLE; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 60dab6cf2b8a..ff169699ebc3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -94,11 +94,6 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } - @Override - protected AnnotationMirror maybeCapability() { - return MAYBE_REPLACEABLE; - } - @Override protected AnnotationMirror positiveCapability() { return REPLACEABLE; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index ee49bdc7ab25..79fdfb231a83 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -80,11 +80,6 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } - @Override - protected AnnotationMirror maybeCapability() { - return MAYBE_SEQ_GROWABLE; - } - @Override protected AnnotationMirror positiveCapability() { return SEQ_GROWABLE; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 1c9e05ffb437..c7d5fdbb54d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -72,11 +72,6 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { postInit(); } - @Override - protected AnnotationMirror maybeCapability() { - return MAYBE_SHRINKABLE; - } - @Override protected AnnotationMirror positiveCapability() { return SHRINKABLE; From c1b38fdafba79fedfd39a7fc2f4b43185be1912b Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 15:31:28 -0700 Subject: [PATCH 231/308] simplified shouldCheckModifiabilityAnnotationValidity --- .../modifiability/ModifiabilityBaseChecker.java | 14 ++++++++++++++ .../modifiability/replace/ReplaceVisitor.java | 6 ------ .../modifiability/seqgrow/SeqGrowVisitor.java | 6 ------ .../modifiability/shrink/ShrinkVisitor.java | 6 ------ 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index c0aa8c2fcf22..fba2932ff635 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -16,4 +16,18 @@ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { /** Creates a new ModifiabilityBaseChecker. */ protected ModifiabilityBaseChecker() {} + + /** + * Returns true if this checker should report diagnostics about annotations whose validity is + * shared across all modifiability sub-checkers. + * + *

    When a sub-checker runs on its own, it should report these diagnostics. When sub-checkers + * run under the aggregate {@link ModifiabilityChecker}, only {@link GrowChecker} reports them to + * avoid duplicate messages. + * + * @return true if this checker should report shared annotation diagnostics + */ + protected boolean shouldCheckModifiabilityAnnotationValidity() { + return getParentChecker() == null || this instanceof GrowChecker; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 1f443814c56e..03dc7ea43e95 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -14,10 +14,4 @@ public class ReplaceVisitor extends ModifiabilityVisitor { public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index bc7aade80dfc..b666ef68226d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -14,10 +14,4 @@ public class SeqGrowVisitor extends ModifiabilityVisitor { public SeqGrowVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index 0682d1138c33..5d58b5382f85 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -14,10 +14,4 @@ public class ShrinkVisitor extends ModifiabilityVisitor { public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } From 7f951cf1cc9b2cbf3d203988a1a2bfd7c8d9971b Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 15:34:33 -0700 Subject: [PATCH 232/308] make last commit complete. --- .../checker/modifiability/ModifiabilityBaseChecker.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index fba2932ff635..79f2ab4e06b8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.modifiability; +import org.checkerframework.checker.modifiability.grow.GrowChecker; import org.checkerframework.common.basetype.BaseTypeChecker; /** @@ -7,10 +8,9 @@ * *

    This class exists so {@link * org.checkerframework.framework.source.SourceChecker#getMessagesProperties()} finds the shared - * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The Grow, - * SeqGrow, Shrink, and Replace checkers all report diagnostics whose message keys are defined - * there; without this common superclass, those keys would need to be duplicated in each - * sub-checker's package. + * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The + * Modifiability sub-checkers all report diagnostics whose message keys are defined there; without + * this common superclass, those keys would need to be duplicated in each sub-checker's package. */ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { From 0df3fc8239f9068a0ca59ceb871b2a3679e355a2 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 16:05:15 -0700 Subject: [PATCH 233/308] revert last two commit beasue the simplification doesn't work. Co-authored-by: Copilot --- .../ModifiabilityBaseChecker.java | 22 ++++--------------- .../modifiability/ModifiabilityVisitor.java | 4 ++-- .../modifiability/replace/ReplaceVisitor.java | 6 +++++ .../modifiability/seqgrow/SeqGrowVisitor.java | 6 +++++ .../modifiability/shrink/ShrinkVisitor.java | 6 +++++ 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index 79f2ab4e06b8..c0aa8c2fcf22 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.modifiability; -import org.checkerframework.checker.modifiability.grow.GrowChecker; import org.checkerframework.common.basetype.BaseTypeChecker; /** @@ -8,26 +7,13 @@ * *

    This class exists so {@link * org.checkerframework.framework.source.SourceChecker#getMessagesProperties()} finds the shared - * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The - * Modifiability sub-checkers all report diagnostics whose message keys are defined there; without - * this common superclass, those keys would need to be duplicated in each sub-checker's package. + * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The Grow, + * SeqGrow, Shrink, and Replace checkers all report diagnostics whose message keys are defined + * there; without this common superclass, those keys would need to be duplicated in each + * sub-checker's package. */ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { /** Creates a new ModifiabilityBaseChecker. */ protected ModifiabilityBaseChecker() {} - - /** - * Returns true if this checker should report diagnostics about annotations whose validity is - * shared across all modifiability sub-checkers. - * - *

    When a sub-checker runs on its own, it should report these diagnostics. When sub-checkers - * run under the aggregate {@link ModifiabilityChecker}, only {@link GrowChecker} reports them to - * avoid duplicate messages. - * - * @return true if this checker should report shared annotation diagnostics - */ - protected boolean shouldCheckModifiabilityAnnotationValidity() { - return getParentChecker() == null || this instanceof GrowChecker; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index bd71716cf441..b3c5ff5768b5 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -185,8 +185,8 @@ protected boolean checkOverride( * Returns true if this checker should report diagnostics about modifiability annotations whose * validity is independent of a particular capability hierarchy. * - *

    The default is {@code true}. SeqGrow, Shrink, and Replace override this to avoid repeating - * diagnostics when running under the aggregate {@link ModifiabilityChecker}. + *

    The default is {@code true}. SeqGrow, Shrink, Replace, and Iterator override this to avoid + * repeating diagnostics when running under the aggregate {@link ModifiabilityChecker}. * * @return true if this visitor should report shared modifiability annotation diagnostics */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 03dc7ea43e95..1f443814c56e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -14,4 +14,10 @@ public class ReplaceVisitor extends ModifiabilityVisitor { public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } + + @Override + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index b666ef68226d..bc7aade80dfc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -14,4 +14,10 @@ public class SeqGrowVisitor extends ModifiabilityVisitor { public SeqGrowVisitor(BaseTypeChecker checker) { super(checker); } + + @Override + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. + return checker.getParentChecker() == null; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index 5d58b5382f85..0682d1138c33 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -14,4 +14,10 @@ public class ShrinkVisitor extends ModifiabilityVisitor { public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } + + @Override + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. + return checker.getParentChecker() == null; + } } From df4ab0305b87f613b63abcbdf067e412e0e091b7 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 5 Jun 2026 16:14:01 -0700 Subject: [PATCH 234/308] Edits to manual --- docs/manual/modifiability-checker.tex | 117 +++++++++++++++++--------- 1 file changed, 79 insertions(+), 38 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index e6a9b034fab0..ed543b8696d5 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -95,22 +95,21 @@ \ do not require a modifiability capability. \noindent -\begin{tabular}{p{0.22\linewidth}p{0.70\linewidth}} +\begin{tabular}{lp{0.75\linewidth}} Capability required & Method families \\ \<@Growable> & - ordinary insertion: \, \, \, + \, \, \, \, \, \, \, timed \, \, \, \, \, \, legacy \ insertion methods, and \ \\ \<@SeqGrowable> & - front/back insertion or positioning (only applicable to sequenced collections and maps): \, \, \, \, \, \, \, \, \, timed \, timed \, \, \ \\ \<@Shrinkable> & - removal: \, \, \, + \, \, \, \, \, \, \, \, \, timed \, \, \, \, \, \, occurrence-removal methods, @@ -118,7 +117,7 @@ \, \, \, and legacy \ removal methods \\ \<@Replaceable> & - replacement without changing size: \, \, + \, \, \, \, \, \, \, and mutating \ algorithms such as \, \, \, @@ -363,6 +362,28 @@ \end{description} +It might seem that it is sufficient to simply annotate \ +directly, as in + +\begin{Verbatim} +class List { + @MaybeShrinkable Iterator iterator() { ... } +} +class ArrayList { + @PolyShrinkable Iterator iterator(@PolyShrinkable ArrayList this) { ... } +} +class CopyOnWriteArrayList { + @Unshrinkable Iterator iterator(@PolyShrinkable ArrayList this) { ... } +} +\end{Verbatim} + +That approach would be sound but imprecise. +In practice, many expressions of static type \ evaluate to +\s or other classes with \<@PolyShrinkable> \ methods. +The \<@IteratorPolyMod> annotation is similar to writing the hypothetical +annotation \<@RuntimeType("ArrayList")>: it indicates a property that is +more precise than the declared type. + \subsectionAndLabel{Polymorphic qualifiers}{modifiability-polymorphic} @@ -439,26 +460,29 @@ capabilities. In such cases, the Modifiability Checker weakens the structurally unavailable component to the corresponding top qualifier. For example, \ has -\ but not \ or \, so \<@Modifiable Iterator> carries -\<@Shrinkable>, but its grow and replace components are weakened to -\<@MaybeGrowable> and \<@MaybeReplaceable>. - +\ but not \ or \, so \<@Modifiable Iterator> expands to +\<@MaybeGrowable @Shrinkable @MaybeReplaceable Iterator>. -\subsectionAndLabel{Alternative design: use negative capability rather than top qualifier}{modifiability-modifyiable-must-be-top} -An alternative design would use the +\subsectionAndLabel{Alternative design: use negative capability rather than top qualifier}{modifiability-modifiable-must-be-top} -Using a top qualifier for a structurally unavailable capability is important. -For example, the static type \ does not declare \ or \, but -\ is a subtype of \ and does declare those methods. -Therefore \<@Modifiable Iterator> should not mean that every possible runtime -iterator is incapable of grow or replace; it means only that those capabilities +An alternative design would make \ expand to +\<@Ungrowable @Shrinkable @Unreplaceable Iterator>. +That would be incorrect, because not every \ is \<@Ungrowable> and \<@Unreplaceable>. +For example, the static type \ does not declare \ or \ +(they are structurally unavailable), but its subtype +\ and does declare those methods. +Therefore \<@Modifiable Iterator> should not mean that every +iterator is incapable of grow and replace; it means only that those capabilities are not known from the static type \. -Similarly, exact \ does not declare replacement methods, but a -\ reference can point to a \, which does support -replacement operations. This behavior ensures that code like this -type-checks: +Similarly, \ does not declare replacement methods, but a variable +with declared type +\ may contain a \, which does support +replacement operations. + +Using the top qualifier for structurally unavailable methods ensures that +code like this type-checks: \begin{Verbatim} @Replaceable LinkedList list = ...; @@ -472,19 +496,29 @@ The following rules define this weakening. The first qualifier in each capability row is used for \<@Modifiable>; the second is used for \<@Unmodifiable>. If the weakening condition holds, both aliases use the -corresponding \<@Maybe*> qualifier instead. - -\begin{tabular}{ll} -Capability & Weakening condition \\ -\/\ & - \ or an \ that is not a \ \\ -\/\ & - not a \, \, or \ \\ -\/\ & - \ \\ -\/\ & - exact \, any \, any non-\ \, - or an \ that is not a \ \\ +corresponding \<@Maybe*> qualifier instead.x + + + +\noindent +\begin{tabular}{l|l|l} +\multicolumn{3}{c}{Expansion for +\<@Modifiable>/\<@Unmodifiable>. ``$\backslash$'' means set difference. A +type includes its subtypes.} \\ \hline +& Usual expansion to \<@>(\)\<*> & Expands to \<@Maybe*> \\ \hline +Grow +& \ +& \ or (\$\backslash$\) \\ +SeqGrow +& \, \, or \ +& not \, \, or \ \\ +Shrink +& not \ +& \ \\ +Replace +& \ or \ +& exact \, any \, (\$\backslash$\), \\ +&& or (\$\backslash$\) \\ \end{tabular} These rules describe alias expansion, not the full specification of every JDK @@ -504,6 +538,7 @@ \noindent \begin{smaller} +\setlength{\tabcolsep}{.33\tabcolsep} \begin{tabular}{rlcrrrrl} \<@Modifiable> & \ & = & \<@Growable> & \<@MaybeSeqGrowable> & \<@Shrinkable> & \<@MaybeReplaceable> & \ \\ \<@Unmodifiable> & \ & = & \<@Ungrowable> & \<@MaybeSeqGrowable> & \<@Unshrinkable> & \<@MaybeReplaceable> & \ \\ @@ -564,7 +599,7 @@ -\sectionAndLabel{Examples}{modifiability-examples} +\sectionAndLabel{Examples of Modifiability Checker warnings}{modifiability-examples} \begin{Verbatim} import java.util.*; @@ -628,14 +663,20 @@ % LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr % LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap -% LocalWords: toArray addAll removeAll retainAll replaceAll setValue +% LocalWords: toArray addAll removeAll retainAll replaceAll setValue Un % LocalWords: MaybeModifiable UnmodifiableParam Growable MaybeGrowable MaybeShrinkable % LocalWords: MaybeReplaceable MaybeSeqGrowable growable seq ListIterator -% LocalWords: PolyModifiable PolySeqGrowable addFirst synchronizedList -% LocalWords: PolyShrinkable PolyGrowable PolyReplaceable SeqGrow +% LocalWords: PolyModifiable PolySeqGrowable addFirst synchronizedList Un +% LocalWords: PolyShrinkable PolyGrowable PolyReplaceable SeqGrow Deque % LocalWords: addLast offerFirst offerLast putFirst putLast unreplaceable seq-growability -% LocalWords: SequencedMap SortedMap +% LocalWords: SequencedMap SortedMap unmodifiableCollection entrySet % LocalWords: SeqGrowable SeqUngrowable BottomSeqGrowable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable % LocalWords: MaybeIteratorPolyMod IteratorPolyMod PreservesModifiability +% LocalWords: BlockingQueue TransferQueue tryTransfer putIfAbsent putAll +% LocalWords: SequencedCollection BlockingDeque removeIf removeFirst +% LocalWords: removeLast pollFirst pollLast NavigableMap pollFirstEntry +% LocalWords: pollLastEntry computeIfPresent drainTo unmodifiability +% LocalWords: seqgrow ConcurrentSkipListSet ConcurrentSkipListMap keySet +% LocalWords: RuntimeType MyClass SortedSet From ff2899ac0307e89a8cfe178b92ef87df9a73f2e4 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 16:17:53 -0700 Subject: [PATCH 235/308] implement iteratorChecker. Co-authored-by: Copilot --- .../ModifiabilityAnnotatedTypeFactory.java | 10 +-- .../modifiability/ModifiabilityChecker.java | 12 +++- .../modifiability/ModifiabilityVisitor.java | 15 ++++ .../IteratorAnnotatedTypeFactory.java | 71 +++++++++++++++++++ .../iterator/IteratorChecker.java | 26 +++++++ .../iterator/IteratorVisitor.java | 31 ++++++++ .../modifiability/iterator/javaparser.astub | 13 ++++ .../modifiability/IteratorCheckerTest.java | 41 +++++++++++ 8 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/iterator/javaparser.astub create mode 100644 checker/tests/modifiability/IteratorCheckerTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index 1db4ae6e3118..b986ccdc2e7a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -34,17 +34,14 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { *

    If the method has no parameters or returns {@code void}, then this annotation has no effect. * *

    Otherwise, if the first argument is positive (i.e.{@code @Shrinkable}), then the return type - * is also positive. If the first argument is {@code @IteratorPolyMod}, then the return type is - * also {@code @IteratorPolyMod}. - * - *

    For every other case, the return type is top ({@code @Maybe*}). + * is also positive. For every other case, the return type is top ({@code @Maybe*}). * *

    Such method cannot be annotated as {@code @Poly*} because an negative (e.g. * {@code @Unshrinkable}) input could yield either a shrinkable or unshrinkable result. It would * be imprecise to always use {@code @Maybe*}, because passing a positive type collection * guarantees a positive return type. * - *

    This method is called by all four sub-checkers. + *

    This method is called by all five sub-checkers. * * @param tree an invocation of a {@code @PreservesModifiability} method * @param methodType the annotated executable type of the invoked method @@ -60,9 +57,6 @@ protected void refinePreservesModifiabilityReturnType( if (argumentType.hasPrimaryAnnotation(positiveCapability())) { returnType.replaceAnnotation(positiveCapability()); } - if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - returnType.replaceAnnotation(ITERATOR_PRESERVE_REMOVE); - } } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 6367976116e2..3a4ad9461091 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.List; import org.checkerframework.checker.modifiability.grow.GrowChecker; +import org.checkerframework.checker.modifiability.iterator.IteratorChecker; import org.checkerframework.checker.modifiability.replace.ReplaceChecker; import org.checkerframework.checker.modifiability.seqgrow.SeqGrowChecker; import org.checkerframework.checker.modifiability.shrink.ShrinkChecker; @@ -13,8 +14,8 @@ * A type-checker that warns, at compile time, if a program might throw {@link * UnsupportedOperationException} at run time due to calling a mutating method on a collection. * - *

    This is an aggregate checker that runs four independent sub-checkers, one for each - * modifiability capability: + *

    This is an aggregate checker that runs independent sub-checkers, one for each modifiability + * capability and one for iterator modifiability preservation: * *

      *
    • {@link GrowChecker} checks grow operations (e.g., {@code add}, {@code addAll}) @@ -22,6 +23,7 @@ * addLast}) *
    • {@link ShrinkChecker} checks shrink operations (e.g., {@code remove}, {@code clear}) *
    • {@link ReplaceChecker} checks replace operations (e.g., {@code set}, {@code replaceAll}) + *
    • {@link IteratorChecker} checks whether iterators preserve receiver modifiability *
    * * @checker_framework.manual #modifiability-checker Modifiability Checker @@ -34,6 +36,10 @@ public ModifiabilityChecker() {} @Override protected Collection> getSupportedCheckers() { return List.of( - GrowChecker.class, SeqGrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); + GrowChecker.class, + SeqGrowChecker.class, + ShrinkChecker.class, + ReplaceChecker.class, + IteratorChecker.class); } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index b3c5ff5768b5..26d06ee9dd93 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -156,6 +156,11 @@ protected boolean checkOverride( overriderTree, overriderMethodType, overriderType, overriddenMethodType, overriddenType)) { return false; } + // Only capability checkers need to preserve receiver capabilities in overrides. + // @IteratorPolyMod does not follow this special override rule. + if (!shouldCheckReceiverOverrideCapabilityPreservation()) { + return true; + } AnnotatedDeclaredType overriderReceiver = overriderMethodType.getReceiverType(); AnnotatedDeclaredType overriddenReceiver = overriddenMethodType.getReceiverType(); @@ -181,6 +186,16 @@ protected boolean checkOverride( return true; } + /** + * Returns true if overrides should preserve positive receiver capabilities from overridden + * methods. + * + * @return true if overrides should preserve positive receiver capabilities + */ + protected boolean shouldCheckReceiverOverrideCapabilityPreservation() { + return true; + } + /** * Returns true if this checker should report diagnostics about modifiability annotations whose * validity is independent of a particular capability hierarchy. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java new file mode 100644 index 000000000000..3776a8269fbb --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -0,0 +1,71 @@ +package org.checkerframework.checker.modifiability.iterator; + +import com.sun.source.tree.MethodInvocationTree; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TreeUtils; + +/** The annotated type factory for the {@link IteratorChecker}. */ +public class IteratorAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { + + /** The {@code @}{@link IteratorPolyMod} qualifier. */ + private final AnnotationMirror ITERATOR_POLY_MOD; + + /** + * Creates an IteratorAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + public IteratorAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); + postInit(); + } + + @Override + protected AnnotationMirror positiveCapability() { + return ITERATOR_POLY_MOD; + } + + @Override + protected AnnotationMirror negativeCapability() { + throw new UnsupportedOperationException("The iterator hierarchy has no negative qualifier."); + } + + @Override + protected AnnotationMirror polyCapability() { + throw new UnsupportedOperationException("The iterator hierarchy has no polymorphic qualifier."); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>(Arrays.asList(MaybeIteratorPolyMod.class, IteratorPolyMod.class)); + } + + @Override + protected ParameterizedExecutableType methodFromUse( + MethodInvocationTree tree, boolean inferTypeArgs) { + ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + AnnotatedExecutableType method = mType.executableType(); + + // if the method is annotated @PreservesModifiability + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refinePreservesModifiabilityReturnType(tree, method); + } + + return mType; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java new file mode 100644 index 000000000000..2674577e8b40 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.modifiability.iterator; + +import java.util.Collection; +import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * A type-checker that checks whether a collection's iterator preserves the collection's + * modifiability. + * + *

    The checker enforces the iterator portion of the Modifiability type system, where + * {@code @IteratorPolyMod} collections have iterators that preserve relevant modifiability + * capabilities and {@code @MaybeIteratorPolyMod} collections have unknown iterator-preservation + * behavior. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({Collection.class}) +@StubFiles({"javaparser.astub"}) +@SuppressWarningsPrefix({"iterator", "modifiability"}) +public class IteratorChecker extends ModifiabilityBaseChecker { + /** Creates an IteratorChecker. */ + public IteratorChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java new file mode 100644 index 000000000000..f9190b743818 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java @@ -0,0 +1,31 @@ +package org.checkerframework.checker.modifiability.iterator; + +import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** Visitor for the {@link IteratorChecker}. */ +public class IteratorVisitor extends ModifiabilityVisitor { + + /** + * Creates a visitor for the Iterator Checker. + * + * @param checker the Iterator Checker + */ + public IteratorVisitor(BaseTypeChecker checker) { + super(checker); + } + + // @IteratorPolyMod does not follow the override capability preservation rules. + // Even if a supertype has @IteratorPolyMod on the receiver, its subtype can override it with an + // implementation that does not use an iterator. + @Override + protected boolean shouldCheckReceiverOverrideCapabilityPreservation() { + return false; + } + + @Override + protected boolean shouldCheckModifiabilityAnnotationValidity() { + // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. + return checker.getParentChecker() == null; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/javaparser.astub new file mode 100644 index 000000000000..9f4fd5199bd5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/javaparser.astub @@ -0,0 +1,13 @@ +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; + +package com.github.javaparser.ast.stmt; + +class BlockStmt { + public @IteratorPolyMod NodeList getStatements() {} +} + +package com.github.javaparser.ast; + +class CompilationUnit { + public @IteratorPolyMod NodeList getImports() {} +} diff --git a/checker/tests/modifiability/IteratorCheckerTest.java b/checker/tests/modifiability/IteratorCheckerTest.java new file mode 100644 index 000000000000..169d39504d33 --- /dev/null +++ b/checker/tests/modifiability/IteratorCheckerTest.java @@ -0,0 +1,41 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; + +class IteratorCheckerTest { + + void iteratorPolyAssignment(@MaybeIteratorPolyMod List maybe) { + // :: error: [assignment] + @IteratorPolyMod List iteratorPoly = maybe; + } + + static class BaseList extends ArrayList { + void requiresIteratorPoly(@IteratorPolyMod BaseList this) {} + } + + static class BadOverride extends BaseList { + @Override + void requiresIteratorPoly(BadOverride this) {} + } + + static class GoodOverride extends BaseList { + @Override + void requiresIteratorPoly(@IteratorPolyMod GoodOverride this) {} + } + + @PreservesModifiability + static List annotated(Collection values) { + return new ArrayList<>(values); + } + + void preservesIteratorPoly( + @IteratorPolyMod List iteratorPoly, @MaybeIteratorPolyMod List maybe) { + @IteratorPolyMod List preserved = annotated(iteratorPoly); + + // :: error: [assignment] + @IteratorPolyMod List notPreserved = annotated(maybe); + } +} From cf8c834b978cbd7fc0e8de9eb3ac99bad59af11b Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 17:06:12 -0700 Subject: [PATCH 236/308] revert changes in refinePreservesModifiabilityReturnType. --- .../modifiability/ModifiabilityAnnotatedTypeFactory.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java index b986ccdc2e7a..594cbb4b689d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java @@ -34,7 +34,10 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { *

    If the method has no parameters or returns {@code void}, then this annotation has no effect. * *

    Otherwise, if the first argument is positive (i.e.{@code @Shrinkable}), then the return type - * is also positive. For every other case, the return type is top ({@code @Maybe*}). + * is also positive. If the first argument is {@code @IteratorPolyMod}, then the return type is + * also {@code @IteratorPolyMod}. + * + *

    For every other case, the return type is top ({@code @Maybe*}). * *

    Such method cannot be annotated as {@code @Poly*} because an negative (e.g. * {@code @Unshrinkable}) input could yield either a shrinkable or unshrinkable result. It would @@ -57,6 +60,9 @@ protected void refinePreservesModifiabilityReturnType( if (argumentType.hasPrimaryAnnotation(positiveCapability())) { returnType.replaceAnnotation(positiveCapability()); } + if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { + returnType.replaceAnnotation(ITERATOR_PRESERVE_REMOVE); + } } /** From 6a66e860a89230a2cea163d1cdd8b16be085f728 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 17:38:14 -0700 Subject: [PATCH 237/308] implement PolyIteratorPolyMod. Co-authored-by: Copilot --- .../qual/PolyIteratorPolyMod.java | 24 +++++++++++++++++++ .../modifiability/qual/PolyModifiable.java | 4 ++-- .../grow/GrowAnnotatedTypeFactory.java | 4 +++- .../IteratorAnnotatedTypeFactory.java | 11 +++++++-- .../replace/ReplaceAnnotatedTypeFactory.java | 4 +++- .../shrink/ShrinkAnnotatedTypeFactory.java | 4 +++- .../modifiability/IteratorCheckerTest.java | 14 +++++++++++ .../modifiability/PolyModifiableTest.java | 13 +++++++--- docs/manual/modifiability-checker.tex | 8 ++++++- 9 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyIteratorPolyMod.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyIteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyIteratorPolyMod.java new file mode 100644 index 000000000000..75c43c0090cb --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyIteratorPolyMod.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.modifiability.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 Iterator hierarchy. + * + *

    When used on a method, whether the return type has {@code @IteratorPolyMod} matches whether + * the argument or receiver annotated with {@code @PolyIteratorPolyMod} has + * {@code @IteratorPolyMod}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(MaybeIteratorPolyMod.class) +public @interface PolyIteratorPolyMod {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index af85d719cf53..9467ac599891 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -5,8 +5,8 @@ /** * Convenience alias meaning - * {@code @PolyGrowable @PolyReplaceable @PolyShrinkable @PolySeqGrowable}. A polymorphic qualifier - * for all four modifiability hierarchies. + * {@code @PolyGrowable @PolyReplaceable @PolyShrinkable @PolySeqGrowable @PolyIteratorPolyMod}. A + * polymorphic qualifier for all five modifiability hierarchies. * *

    Write {@code @PolyModifiable} on methods that preserve or transfer modifiability, such as * {@code List.subList()}. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 99cc7b04075a..921469d3f611 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -18,6 +18,7 @@ import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrowable; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Ungrowable; @@ -103,7 +104,8 @@ protected Set> createSupportedTypeQualifiers() { BottomGrowable.class, PolyGrowable.class, MaybeIteratorPolyMod.class, - IteratorPolyMod.class)); + IteratorPolyMod.class, + PolyIteratorPolyMod.class)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index 3776a8269fbb..34735f7c661f 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -10,6 +10,7 @@ import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -22,6 +23,9 @@ public class IteratorAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFact /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_POLY_MOD; + /** The {@code @}{@link PolyIteratorPolyMod} qualifier. */ + private final AnnotationMirror POLY_ITERATOR_POLY_MOD; + /** * Creates an IteratorAnnotatedTypeFactory. * @@ -31,6 +35,7 @@ public class IteratorAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFact public IteratorAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); + this.POLY_ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, PolyIteratorPolyMod.class); postInit(); } @@ -46,12 +51,14 @@ protected AnnotationMirror negativeCapability() { @Override protected AnnotationMirror polyCapability() { - throw new UnsupportedOperationException("The iterator hierarchy has no polymorphic qualifier."); + return POLY_ITERATOR_POLY_MOD; } @Override protected Set> createSupportedTypeQualifiers() { - return new LinkedHashSet<>(Arrays.asList(MaybeIteratorPolyMod.class, IteratorPolyMod.class)); + return new LinkedHashSet<>( + Arrays.asList( + MaybeIteratorPolyMod.class, IteratorPolyMod.class, PolyIteratorPolyMod.class)); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index ff169699ebc3..79d55c7f7dc0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -16,6 +16,7 @@ import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplaceable; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; @@ -119,7 +120,8 @@ protected Set> createSupportedTypeQualifiers() { BottomReplaceable.class, PolyReplaceable.class, MaybeIteratorPolyMod.class, - IteratorPolyMod.class)); + IteratorPolyMod.class, + PolyIteratorPolyMod.class)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index c7d5fdbb54d2..72534dfe13eb 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -15,6 +15,7 @@ import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; @@ -97,7 +98,8 @@ protected Set> createSupportedTypeQualifiers() { BottomShrinkable.class, PolyShrinkable.class, MaybeIteratorPolyMod.class, - IteratorPolyMod.class)); + IteratorPolyMod.class, + PolyIteratorPolyMod.class)); } /** diff --git a/checker/tests/modifiability/IteratorCheckerTest.java b/checker/tests/modifiability/IteratorCheckerTest.java index 169d39504d33..06b5fc3f1259 100644 --- a/checker/tests/modifiability/IteratorCheckerTest.java +++ b/checker/tests/modifiability/IteratorCheckerTest.java @@ -3,6 +3,7 @@ import java.util.List; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; class IteratorCheckerTest { @@ -31,6 +32,11 @@ static List annotated(Collection values) { return new ArrayList<>(values); } + static @PolyIteratorPolyMod List identityIteratorPoly( + @PolyIteratorPolyMod List values) { + return values; + } + void preservesIteratorPoly( @IteratorPolyMod List iteratorPoly, @MaybeIteratorPolyMod List maybe) { @IteratorPolyMod List preserved = annotated(iteratorPoly); @@ -38,4 +44,12 @@ void preservesIteratorPoly( // :: error: [assignment] @IteratorPolyMod List notPreserved = annotated(maybe); } + + void polyIteratorPoly( + @IteratorPolyMod List iteratorPoly, @MaybeIteratorPolyMod List maybe) { + @IteratorPolyMod List preserved = identityIteratorPoly(iteratorPoly); + + // :: error: [assignment] + @IteratorPolyMod List notPreserved = identityIteratorPoly(maybe); + } } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index f4d152ce6ecd..5e54b20a874d 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,8 +1,10 @@ import java.util.Deque; import java.util.List; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.SeqGrowable; @@ -12,13 +14,14 @@ /** * Tests @PolyModifiable, which expands - * to @PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable and thus preserves all four - * capabilities independently. + * to @PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable @PolyIteratorPolyMod and thus + * preserves all five capabilities independently. */ public class PolyModifiableTest { /** A simple polymorphic identity method that preserves all four capabilities. */ - @PolyModifiable List identity(@PolyModifiable List x) { + @PolyModifiable @PolyIteratorPolyMod + List identity(@PolyModifiable @PolyIteratorPolyMod List x) { return x; } @@ -29,6 +32,7 @@ public class PolyModifiableTest { void testPoly( @Modifiable List mod, + @Modifiable @IteratorPolyMod List modIteratorPoly, @Growable @Shrinkable List gs, // Grow+Shrink; Replace = MaybeReplaceable (default) @Growable @Replaceable List gr, // Grow+Replace; Shrink = MaybeShrinkable @Shrinkable @Replaceable List sr, // Shrink+Replace; Grow = MaybeGrowable @@ -43,6 +47,9 @@ void testPoly( // ============================================================ @Modifiable List m1 = identity(mod); // OK: poly resolves to @Modifiable @MaybeModifiable List m2 = identity(mod); // OK: positive capabilities <: tops + @IteratorPolyMod List m3 = identity(modIteratorPoly); // OK: preserves iterator poly + // :: error: [assignment] + @IteratorPolyMod List m4 = identity(mod); // ============================================================ // Identity on @Growable @Shrinkable (G+S; Seq=MaybeSeqGrowable, R=MaybeReplaceable) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index ed543b8696d5..c406de9e8155 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -416,6 +416,12 @@ The return type's replaceability matches the replaceability of whichever formal parameter is annotated with \<@PolyReplaceable>. +\item[\refqualclass{checker/modifiability/qual}{PolyIteratorPolyMod}] +Polymorphic qualifier for the Iterator hierarchy. +The return type's iterator-preservation qualifier matches the +iterator-preservation qualifier of whichever formal parameter is annotated +with \<@PolyIteratorPolyMod>. + \item[\refqualclass{checker/modifiability/qual}{PolyModifiable}] Alias for \code{@PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable}. Preserves all four capabilities simultaneously. @@ -673,7 +679,7 @@ % LocalWords: SeqGrowable SeqUngrowable BottomSeqGrowable Growability shrinkability replacability % LocalWords: growability Shrinkable Replaceable Ungrowable Unshrinkable % LocalWords: Unreplaceable BottomGrowable BottomShrinkable BottomReplaceable -% LocalWords: MaybeIteratorPolyMod IteratorPolyMod PreservesModifiability +% LocalWords: MaybeIteratorPolyMod IteratorPolyMod PolyIteratorPolyMod PreservesModifiability % LocalWords: BlockingQueue TransferQueue tryTransfer putIfAbsent putAll % LocalWords: SequencedCollection BlockingDeque removeIf removeFirst % LocalWords: removeLast pollFirst pollLast NavigableMap pollFirstEntry From d51a2f419abf8f4373e76f663a9b03187b500ad5 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 17:52:00 -0700 Subject: [PATCH 238/308] start annotating @PolyIteratorPolyMod --- .../tests/modifiability/CollectionsModifiableTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index c599cc1fbbe1..bbfb0402a693 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -55,11 +55,11 @@ void testBinarySearch() { void testCheckedWrappers() { @Modifiable Collection c = new ArrayList<>(); @Modifiable Collection checkedC = Collections.checkedCollection(c, String.class); - checkedC.addAll(List.of("a")); + checkedC.add("a"); @Modifiable List l = new ArrayList<>(); @Modifiable List checkedL = Collections.checkedList(l, String.class); - checkedL.addAll(List.of("a")); + checkedL.add("a"); @Modifiable Map m = new HashMap<>(); @Modifiable Map checkedM = Collections.checkedMap(m, String.class, String.class); @@ -253,11 +253,11 @@ void testSynchronizedWrappers() { // Synchronized wrappers pass through modifiability @Modifiable Collection c = new ArrayList<>(); @Modifiable Collection syncC = Collections.synchronizedCollection(c); - syncC.addAll(List.of("a")); + syncC.add("a"); @Modifiable List l = new ArrayList<>(); @Modifiable List syncL = Collections.synchronizedList(l); - syncL.addAll(List.of("a")); + syncL.add("a"); } void testUnmodifiableWrappers() { From 22dec0a68ec6c86ab8f277186d78cfff5be8c917 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Fri, 5 Jun 2026 17:55:59 -0700 Subject: [PATCH 239/308] fixed a nullability bug --- .../checker/modifiability/ModifiabilityVisitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 26d06ee9dd93..d64ad5c26708 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -117,7 +117,7 @@ public void processMethodTree(String className, MethodTree tree) { public Void visitAnnotation(AnnotationTree tree, Void p) { if (shouldCheckModifiabilityAnnotationValidity() && isUnmodifiableParamAnnotation(tree)) { if (allowedUnmodifiableParamAnnotations.isEmpty() - || !allowedUnmodifiableParamAnnotations.peek().contains(tree)) { + || !allowedUnmodifiableParamAnnotations.getFirst().contains(tree)) { checker.reportError(tree, "unmodparam.location"); } } From 1398ca0f32848f069b8961d344fc67c8af4b7125 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Mon, 8 Jun 2026 11:12:17 -0700 Subject: [PATCH 240/308] Tweaks to PreservesModifiability. --- docs/manual/modifiability-checker.tex | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index c406de9e8155..62667816ca56 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -439,18 +439,21 @@ \begin{description} \item[\refqualclass{checker/modifiability/qual}{PreservesModifiability}] -Should only be written on a non-void, one-argument method declaration. -It indicates that the -method preserves positive modifiability capabilities from its argument to -its return value. If the argument is \<@Growable>, \<@Shrinkable>, +Indicates that if the argument to the method is modifiable, then the returned collection is +also modifiable. However, if the argument is unmodifiable, then the returned collection may or may +not be unmodifiable. This is useful for methods that may return their argument, or may return a +new modifiable collection. +For example, if the argument is \<@Growable>, then the returned collection is also +\<@Growable>. But if the argument is \<@Ungrowable>, then the return collection is \<@MaybeGrowable>. +More generally, if the argument is \<@Growable>, \<@Shrinkable>, \<@Replaceable>, \<@SeqGrowable>, or \<@IteratorPolyMod>, then the Modifiability Checker treats the return value as having that same capability. If the argument has any other qualifier, the return type is the top qualifier (\<@MaybeGrowable>, \<@MaybeSeqGrowable>, \<@MaybeShrinkable>, \<@MaybeReplaceable>, or \<@MaybeIteratorPolyMod>) in the corresponding hierarchy. -This is useful for methods that may return their argument, or may return a -new modifiable collection. +This annotation should only be written on non-void, one-argument method declarations. + \end{description} \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, seq-grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} From 90f83e83623c5c8e15bd817b85f957dd26f2d278 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 8 Jun 2026 15:02:54 -0700 Subject: [PATCH 241/308] test out annotation on getOrDefault. Co-authored-by: Copilot --- .../tests/modifiability/GetOrDefaultTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/checker/tests/modifiability/GetOrDefaultTest.java b/checker/tests/modifiability/GetOrDefaultTest.java index 25fca75fcd6b..58fa7d848d3c 100644 --- a/checker/tests/modifiability/GetOrDefaultTest.java +++ b/checker/tests/modifiability/GetOrDefaultTest.java @@ -10,6 +10,10 @@ interface MyGodMap { // With @PolyModifiable, which seems wrong. @PolyModifiable V getOrDefaultPoly(MyGodMap this, Object key, @PolyModifiable V defaultValue); + // With @PolyModifiable on all three, this seems correct. + @PolyModifiable V getOrDefaultPoly2( + MyGodMap this, Object key, @PolyModifiable V defaultValue); + // Using a second type variable. V2 getOrDefault2(MyGodMap this, Object key, V2 defaultValue); } @@ -211,4 +215,39 @@ void foo9(MyGodMap> m) { @Modifiable List d8 = m.getOrDefault2("hello", du); @Unmodifiable List d9 = m.getOrDefault2("hello", du); } + + // this is the desired behavior. + void foo10(MyGodMap> m) { + List d1 = m.getOrDefaultPoly2("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d2 = m.getOrDefaultPoly2("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d3 = m.getOrDefaultPoly2("hello", d); + List d4 = m.getOrDefaultPoly2("hello", dm); + @Modifiable List d5 = m.getOrDefaultPoly2("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefaultPoly2("hello", dm); + List d7 = m.getOrDefaultPoly2("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = m.getOrDefaultPoly2("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d9 = m.getOrDefaultPoly2("hello", du); + } + + void foo11(MyGodMap> um) { + List d1 = um.getOrDefaultPoly2("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d2 = um.getOrDefaultPoly2("hello", d); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d3 = um.getOrDefaultPoly2("hello", d); + List d4 = um.getOrDefaultPoly2("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d5 = um.getOrDefaultPoly2("hello", dm); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Unmodifiable List d6 = um.getOrDefaultPoly2("hello", dm); + List d7 = um.getOrDefaultPoly2("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = um.getOrDefaultPoly2("hello", du); + @Unmodifiable List d9 = um.getOrDefaultPoly2("hello", du); + } } From 406a10bd39c0e261570edb4a1754674ae10bb1c8 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 8 Jun 2026 18:02:25 -0700 Subject: [PATCH 242/308] test annotation on getOrDefault. --- .../modifiability/MapModifiableTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/checker/tests/modifiability/MapModifiableTest.java b/checker/tests/modifiability/MapModifiableTest.java index 8a83eb044b30..61267fa7b4d9 100644 --- a/checker/tests/modifiability/MapModifiableTest.java +++ b/checker/tests/modifiability/MapModifiableTest.java @@ -1,5 +1,6 @@ import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -10,6 +11,10 @@ // Tests for Map mutating and read-only methods with modifiability qualifiers. public class MapModifiableTest { + List d; + @Modifiable List dm; + @Unmodifiable List du; + void testBasicMutatingOperations() { @Modifiable Map mod = new HashMap<>(); mod.put("a", 1); @@ -106,4 +111,39 @@ void testAnnotatedLocalsFromParams( // :: error: [method.invocation] any.put("q", 2); } + + // this is the desired behavior. + void foo10(Map> m) { + List d1 = m.getOrDefault("hello", d); + // :: error: [assignment] + @Modifiable List d2 = m.getOrDefault("hello", d); + // :: error: [assignment] + @Unmodifiable List d3 = m.getOrDefault("hello", d); + List d4 = m.getOrDefault("hello", dm); + @Modifiable List d5 = m.getOrDefault("hello", dm); + // :: error: [assignment] + @Unmodifiable List d6 = m.getOrDefault("hello", dm); + List d7 = m.getOrDefault("hello", du); + // :: error: [assignment] + @Modifiable List d8 = m.getOrDefault("hello", du); + // :: error: [assignment] + @Unmodifiable List d9 = m.getOrDefault("hello", du); + } + + void foo11(Map> um) { + List d1 = um.getOrDefault("hello", d); + // :: error: [assignment] + @Modifiable List d2 = um.getOrDefault("hello", d); + // :: error: [assignment] + @Unmodifiable List d3 = um.getOrDefault("hello", d); + List d4 = um.getOrDefault("hello", dm); + // :: error: [assignment] + @Modifiable List d5 = um.getOrDefault("hello", dm); + // :: error: [assignment] + @Unmodifiable List d6 = um.getOrDefault("hello", dm); + List d7 = um.getOrDefault("hello", du); + // :: error: [assignment] :: error: [assignment] :: error: [assignment] + @Modifiable List d8 = um.getOrDefault("hello", du); + @Unmodifiable List d9 = um.getOrDefault("hello", du); + } } From 006eb3c6a643924077d6c1ee565a3401a44b7f88 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Mon, 8 Jun 2026 18:04:46 -0700 Subject: [PATCH 243/308] test out super.invocation error vs annotation.unverified. --- .../modifiability/CustomModifiabilityAnnotationWarning.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index 67bab3a237bd..15c4c4160235 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -1,4 +1,5 @@ import java.util.AbstractList; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.ListIterator; @@ -50,6 +51,11 @@ public int size() { } } + // The implicit super() call is fine because ArrayList's constructor already + // returns @Modifiable, but the subclass annotation is still an unverified modifiability claim. + // :: warning: [modifiability.annotation.unverified] + static @Modifiable class ModifiableSubclassWithoutSuperError extends ArrayList {} + // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] abstract static @Growable class ClassLevelGrowableCollection implements Collection {} From 0c82f19ce5f8c09c72520ceb2bbb09890c227176 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 9 Jun 2026 13:36:35 -0700 Subject: [PATCH 244/308] clarifies the manual --- docs/manual/modifiability-checker.tex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index c406de9e8155..bc888ee3fc24 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -27,6 +27,10 @@ (For brevity, this chapter generally uses just ``collection'' to mean ``collection, map, or iterator''.) +In addition to preventing UOEs, the Modifiability Checker ensures that collections +that the user intends to be unmodifiable have an unmodifiable run-time implementation. + + To run the Modifiability Checker: \begin{Verbatim} @@ -146,7 +150,6 @@ 2-element hierarchy to express whether \ can be called on a collection's iterator. -% TODO: add a figure that follows the structure of other checker manuals. \begin{figure} \begin{smaller} \centering From 977df2f81241a47b640f2ef33e86f650f68cbab0 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 10 Jun 2026 14:07:50 -0700 Subject: [PATCH 245/308] Suzanne's code review. --- ...odifiabilityBaseAnnotatedTypeFactory.java} | 9 ++++---- ...tor.java => ModifiabilityBaseVisitor.java} | 21 +++++++------------ .../grow/GrowAnnotatedTypeFactory.java | 4 ++-- .../modifiability/grow/GrowVisitor.java | 4 ++-- .../IteratorAnnotatedTypeFactory.java | 4 ++-- .../iterator/IteratorVisitor.java | 4 ++-- .../replace/ReplaceAnnotatedTypeFactory.java | 5 ++--- .../modifiability/replace/ReplaceVisitor.java | 4 ++-- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 4 ++-- .../modifiability/seqgrow/SeqGrowVisitor.java | 4 ++-- .../shrink/ShrinkAnnotatedTypeFactory.java | 4 ++-- .../modifiability/shrink/ShrinkVisitor.java | 4 ++-- 12 files changed, 32 insertions(+), 39 deletions(-) rename checker/src/main/java/org/checkerframework/checker/modifiability/{ModifiabilityAnnotatedTypeFactory.java => ModifiabilityBaseAnnotatedTypeFactory.java} (95%) rename checker/src/main/java/org/checkerframework/checker/modifiability/{ModifiabilityVisitor.java => ModifiabilityBaseVisitor.java} (96%) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java similarity index 95% rename from checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java rename to checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 594cbb4b689d..ac536a176254 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -13,17 +13,16 @@ import org.checkerframework.javacutil.TreeUtils; /** Shared annotated type factory logic for the Modifiability sub-checkers. */ -public abstract class ModifiabilityAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link IteratorPolyMod} qualifier. */ protected final AnnotationMirror ITERATOR_PRESERVE_REMOVE; /** - * Creates a ModifiabilityAnnotatedTypeFactory. + * Creates a ModifiabilityBaseAnnotatedTypeFactory. * * @param checker the associated type-checker */ - @SuppressWarnings("this-escape") - protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { + protected ModifiabilityBaseAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); } @@ -39,7 +38,7 @@ protected ModifiabilityAnnotatedTypeFactory(BaseTypeChecker checker) { * *

    For every other case, the return type is top ({@code @Maybe*}). * - *

    Such method cannot be annotated as {@code @Poly*} because an negative (e.g. + *

    Such method cannot be annotated as {@code @Poly*} because a negative (e.g. * {@code @Unshrinkable}) input could yield either a shrinkable or unshrinkable result. It would * be imprecise to always use {@code @Maybe*}, because passing a positive type collection * guarantees a positive return type. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java similarity index 96% rename from checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java rename to checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index d64ad5c26708..160f9350bccc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -34,7 +34,8 @@ * legitimately produce {@code @Modifiable}. *

*/ -public class ModifiabilityVisitor extends BaseTypeVisitor { +public class ModifiabilityBaseVisitor + extends BaseTypeVisitor { /** The erased {@code java.util.Collection} type. */ private final TypeMirror collectionErasure; @@ -53,11 +54,11 @@ public class ModifiabilityVisitor extends BaseTypeVisitorThe framework's ordinary receiver override rule allows an overriding method to relax * receiver preconditions. For modifiability operations, that would allow a subtype method to drop * a required {@code @Growable}, {@code @Shrinkable}, or {@code @Replaceable} receiver capability. + * TODO: I don't understand why that's bad? Is it because the checker can't verify collection + * implementations. */ @Override protected boolean checkOverride( @@ -294,11 +297,7 @@ private boolean isWarningModifiabilityAnnotation(AnnotationMirror annotation) { // Maybe* annotations and @UnmodifiableParam are unknown/top-like, so they do not warn. String simpleName = typeElement.getSimpleName().toString(); - if (simpleName.startsWith("Maybe") || simpleName.equals("UnmodifiableParam")) { - return false; - } - - return true; + return !simpleName.startsWith("Maybe") && !simpleName.equals("UnmodifiableParam"); } /** @@ -325,11 +324,7 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { // Scan both javac locations for parameter type annotations. // search for method(@UnmodifiableParam List<> param) - scanner.scan(parameter.getModifiers().getAnnotations(), null); - if (parameter.getType() != null) { - // search for method(List<@UnmodifiableParam List<>> param) - scanner.scan(parameter.getType(), null); - } + scanner.scan(parameter, null); return annotations; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 921469d3f611..c7da91eff1d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -9,7 +9,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; @@ -32,7 +32,7 @@ import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link GrowChecker}. */ -public class GrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { +public class GrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryErasure; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java index b0e4346eb142..411b9f24ab96 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java @@ -1,10 +1,10 @@ package org.checkerframework.checker.modifiability.grow; -import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; /** Visitor for the {@link GrowChecker}. */ -public class GrowVisitor extends ModifiabilityVisitor { +public class GrowVisitor extends ModifiabilityBaseVisitor { /** * Creates a visitor for the Grow Checker. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index 34735f7c661f..2115bf7b1bb6 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -7,7 +7,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; @@ -18,7 +18,7 @@ import org.checkerframework.javacutil.TreeUtils; /** The annotated type factory for the {@link IteratorChecker}. */ -public class IteratorAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { +public class IteratorAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_POLY_MOD; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java index f9190b743818..12b8ff131d8c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java @@ -1,10 +1,10 @@ package org.checkerframework.checker.modifiability.iterator; -import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; /** Visitor for the {@link IteratorChecker}. */ -public class IteratorVisitor extends ModifiabilityVisitor { +public class IteratorVisitor extends ModifiabilityBaseVisitor { /** * Creates a visitor for the Iterator Checker. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 79d55c7f7dc0..332403d5bc23 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -9,7 +9,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -26,14 +26,13 @@ import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ReplaceChecker}. */ -public class ReplaceAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { +public class ReplaceAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { /** The erased {@code java.util.Set} type. */ private final TypeMirror setErasure; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index 1f443814c56e..aa4c78ed2e75 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -1,10 +1,10 @@ package org.checkerframework.checker.modifiability.replace; -import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; /** Visitor for the {@link ReplaceChecker}. */ -public class ReplaceVisitor extends ModifiabilityVisitor { +public class ReplaceVisitor extends ModifiabilityBaseVisitor { /** * Creates a visitor for the Replace Checker. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 79fdfb231a83..c3aab0381848 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -10,7 +10,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomSeqGrowable; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; @@ -30,7 +30,7 @@ import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link SeqGrowChecker}. */ -public class SeqGrowAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { +public class SeqGrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { /** The erased {@code java.util.SequencedCollection} type. */ private final @Nullable TypeMirror sequencedCollectionErasure; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index bc7aade80dfc..91c21b65cb01 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -1,10 +1,10 @@ package org.checkerframework.checker.modifiability.seqgrow; -import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; /** Visitor for the {@link SeqGrowChecker}. */ -public class SeqGrowVisitor extends ModifiabilityVisitor { +public class SeqGrowVisitor extends ModifiabilityBaseVisitor { /** * Creates a visitor for the SeqGrow Checker. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 72534dfe13eb..ab6b3c52df04 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -8,7 +8,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.modifiability.ModifiabilityAnnotatedTypeFactory; +import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -31,7 +31,7 @@ import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ShrinkChecker}. */ -public class ShrinkAnnotatedTypeFactory extends ModifiabilityAnnotatedTypeFactory { +public class ShrinkAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { /** The erased {@code java.util.Map.Entry} type. */ private final TypeMirror mapEntryErasure; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index 0682d1138c33..e7c4f0eabf6b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -1,10 +1,10 @@ package org.checkerframework.checker.modifiability.shrink; -import org.checkerframework.checker.modifiability.ModifiabilityVisitor; +import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; import org.checkerframework.common.basetype.BaseTypeChecker; /** Visitor for the {@link ShrinkChecker}. */ -public class ShrinkVisitor extends ModifiabilityVisitor { +public class ShrinkVisitor extends ModifiabilityBaseVisitor { /** * Create a visitor for the Shrink Checker. From 63d977ddbb9e60417dc2ebc383ea15a39babae1c Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 10 Jun 2026 16:08:38 -0700 Subject: [PATCH 246/308] add annotation.unverified to the manual. --- docs/manual/modifiability-checker.tex | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 9d5d24def771..cad9a5506af7 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -609,6 +609,33 @@ it3.remove(); // Error: list3 does not have @IteratorPolyMod \end{Verbatim} +\sectionAndLabel{Unverified collection implementation annotations}{modifiability-annotation-unverified} + +The Modifiability Checker verifies uses of collection modifiability annotations, +but it does not verify collection implementations. + +When a user-defined collection-like class (a \, \, +\, or subtype) explicitly writes a concrete modifiability qualifier +on the class declaration or on a constructor, the checker issues a +\ warning. The warning means that the +annotation is a trusted specification of the custom implementation, not a fact +verified from the implementation's method bodies. After examining the +implementation, suppress the warning with +\<@SuppressWarnings("modifiability:annotation.unverified")>. + +For example, the class-level \<@Growable> annotation below triggers +\. It is up to the programmer to verify that +the implementation of \ is not \@Growable and remove it from the class declaration. +\begin{Verbatim} +public @Growable class TestMod extends ArrayList { + + @Override + public boolean add(@Growable TestMod this, String s) { + throw new UnsupportedOperationException(); + } +} +\end{Verbatim} + \sectionAndLabel{Examples of Modifiability Checker warnings}{modifiability-examples} From 7e508c3976994b8e63927335c3bc1fd7661a6bf5 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 10 Jun 2026 16:25:41 -0700 Subject: [PATCH 247/308] fixed unmodArrayListTest Co-authored-by: Copilot --- .../modifiability/UnmodArrayListTest.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/checker/tests/modifiability/UnmodArrayListTest.java b/checker/tests/modifiability/UnmodArrayListTest.java index fe19c7b75fba..759e45b95479 100644 --- a/checker/tests/modifiability/UnmodArrayListTest.java +++ b/checker/tests/modifiability/UnmodArrayListTest.java @@ -1,16 +1,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; -/** - * Demonstrates a known soundness hole: casting an @Unmodifiable list to a concrete @Modifiable - * class (ArrayList) bypasses the checker's protection. - * - *

The checker sees ArrayList as @Modifiable by default, so the downcast is treated as "safe" and - * the subsequent add() call raises no warning — even though the underlying object is an - * unmodifiable wrapper that will throw UnsupportedOperationException at runtime. - */ public class UnmodArrayListTest { void testUnmodifiableCastEscape() { @@ -20,13 +13,18 @@ void testUnmodifiableCastEscape() { // Checker correctly infers @Unmodifiable here. @Unmodifiable List view = Collections.unmodifiableList(myList); - // Because ArrayList is treated as @Modifiable by the checker, this downcast is considered - // "safe" — no cast.unsafe warning is issued, even though the underlying object is an - // unmodifiable wrapper and will throw ClassCastException at runtime. ArrayList backToMod = (ArrayList) view; - // The checker allows this because backToMod is typed as @Modifiable ArrayList. // :: error: [method.invocation] backToMod.add("Boom"); } + + void test(@Modifiable ArrayList tests) { + tests.add("a"); + } + + void test2(ArrayList tests) { + // :: error: [method.invocation] + tests.add("a"); + } } From 7860edbcf5c42e08e5f7383c60284a8a88f20fd9 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 11 Jun 2026 11:46:05 -0700 Subject: [PATCH 248/308] Make the IteratorChecker a subchecker. --- .../ModifiabilityBaseAnnotatedTypeFactory.java | 9 ++++++--- .../modifiability/ModifiabilityBaseChecker.java | 13 +++++++++++++ .../grow/GrowAnnotatedTypeFactory.java | 8 +------- .../checker/modifiability/grow/javaparser.astub | 5 ++--- .../replace/ReplaceAnnotatedTypeFactory.java | 8 +------- .../checker/modifiability/replace/javaparser.astub | 5 ++--- .../shrink/ShrinkAnnotatedTypeFactory.java | 8 +------- .../checker/modifiability/shrink/javaparser.astub | 5 ++--- 8 files changed, 28 insertions(+), 33 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index ac536a176254..23e41c1477d4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -4,6 +4,7 @@ import com.sun.source.tree.Tree; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.modifiability.iterator.IteratorChecker; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -109,9 +110,11 @@ protected void refineIteratorReturnType( } // receiver type is @Modifiable and @IteratorPolyMod - if (receiverType.hasPrimaryAnnotation(positiveCapability()) - && receiverType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - returnType.replaceAnnotation(positiveCapability()); + if (receiverType.hasPrimaryAnnotation(positiveCapability())) { + var type = getTypeFactoryOfSubchecker(IteratorChecker.class).getAnnotatedType(receiverTree); + if (type.hasAnnotation(ITERATOR_PRESERVE_REMOVE)) { + returnType.replaceAnnotation(positiveCapability()); + } } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index c0aa8c2fcf22..e97d27168d8e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -1,6 +1,9 @@ package org.checkerframework.checker.modifiability; +import java.util.Set; +import org.checkerframework.checker.modifiability.iterator.IteratorChecker; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.source.SourceChecker; /** * Base class for the Modifiability sub-checkers. @@ -16,4 +19,14 @@ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { /** Creates a new ModifiabilityBaseChecker. */ protected ModifiabilityBaseChecker() {} + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + if (this instanceof IteratorChecker) { + return checkers; + } + checkers.add(IteratorChecker.class); + return checkers; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index c7da91eff1d2..9312dd09df7c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -12,13 +12,10 @@ import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrowable; -import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Ungrowable; @@ -102,10 +99,7 @@ protected Set> createSupportedTypeQualifiers() { Growable.class, Ungrowable.class, BottomGrowable.class, - PolyGrowable.class, - MaybeIteratorPolyMod.class, - IteratorPolyMod.class, - PolyIteratorPolyMod.class)); + PolyGrowable.class)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub index 2b5494331d72..cc63c87a8309 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub @@ -1,14 +1,13 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; class BlockStmt { - public @Modifiable @IteratorPolyMod NodeList getStatements() {} + public @Modifiable NodeList getStatements() {} } package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyMod NodeList getImports() {} + public @Modifiable NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 332403d5bc23..cb9989b02599 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -11,12 +11,9 @@ import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplaceable; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; @@ -117,10 +114,7 @@ protected Set> createSupportedTypeQualifiers() { Replaceable.class, Unreplaceable.class, BottomReplaceable.class, - PolyReplaceable.class, - MaybeIteratorPolyMod.class, - IteratorPolyMod.class, - PolyIteratorPolyMod.class)); + PolyReplaceable.class)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub index 2b5494331d72..cc63c87a8309 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub @@ -1,14 +1,13 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; class BlockStmt { - public @Modifiable @IteratorPolyMod NodeList getStatements() {} + public @Modifiable NodeList getStatements() {} } package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyMod NodeList getImports() {} + public @Modifiable NodeList getImports() {} } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index ab6b3c52df04..6ab93fee870d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -10,12 +10,9 @@ import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; @@ -96,10 +93,7 @@ protected Set> createSupportedTypeQualifiers() { Shrinkable.class, Unshrinkable.class, BottomShrinkable.class, - PolyShrinkable.class, - MaybeIteratorPolyMod.class, - IteratorPolyMod.class, - PolyIteratorPolyMod.class)); + PolyShrinkable.class)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub index 2b5494331d72..cc63c87a8309 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub @@ -1,14 +1,13 @@ import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; package com.github.javaparser.ast.stmt; class BlockStmt { - public @Modifiable @IteratorPolyMod NodeList getStatements() {} + public @Modifiable NodeList getStatements() {} } package com.github.javaparser.ast; class CompilationUnit { - public @Modifiable @IteratorPolyMod NodeList getImports() {} + public @Modifiable NodeList getImports() {} } From 73c654157ef7fcf5c6cbc2e1cdbcd99f86e539db Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 11 Jun 2026 15:41:02 -0700 Subject: [PATCH 249/308] fixed CustomModifiabilityAnnotationWarning test. --- .../CustomModifiabilityAnnotationWarning.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index 15c4c4160235..bd8847611fcc 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -62,7 +62,7 @@ public int size() { // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] abstract static @Unmodifiable class ClassLevelUnmodifiableMap implements Map {} - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator implements Iterator {} @@ -142,12 +142,7 @@ public int size() { } @Override - // Only ShrinkChecker and ReplaceChecker report this receiver override. - // The class-level @Modifiable makes SuppressedList @SeqGrowable too, but - // SeqGrowChecker ignores @IteratorPolyMod. As a result, the receiver appears - // to match SuppressedList's declaration bound in the SeqGrow hierarchy, so - // BaseTypeVisitor's declaration-bound exception accepts the override. - // :: error: [override.receiver] + // no [override.receiver] error here because @Modifiable is an upper bound. public boolean add(@IteratorPolyMod SuppressedList this, String e) { return super.add(e); } @@ -155,7 +150,4 @@ public boolean add(@IteratorPolyMod SuppressedList this, String e) { // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] abstract static @Unmodifiable class SuppressedMap implements Map {} - - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] - abstract static @IteratorPolyMod class SuppressedIterator implements Iterator {} } From 35b5c8f7cf361353361b762669e59f2cff134e2a Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 11 Jun 2026 16:11:19 -0700 Subject: [PATCH 250/308] test out defaultQualifierForUse Co-authored-by: Copilot --- .../ClassAnnosVSDefaultQualifierForUse.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java diff --git a/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java new file mode 100644 index 000000000000..bd00408f4975 --- /dev/null +++ b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java @@ -0,0 +1,34 @@ +import java.util.ArrayList; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; +import org.checkerframework.framework.qual.DefaultQualifierForUse; + +public class ClassAnnosVSDefaultQualifierForUse { + + // write annotation on class declaration. + // :: warning: [modifiability.annotation.unverified] + static @Growable class ModList extends ArrayList {} + + // :: error: [annotations.on.use] + @MaybeGrowable ModList list = new ModList<>(); + + void test1(ModList tests) { + tests.add("a"); + } + + // :: error: [annotations.on.use] + void foo(@UnmodifiableParam ModList list) {} + + // use @DefaultQualifierForUse + @DefaultQualifierForUse(Growable.class) + static class DefaultList extends ArrayList {} + + @MaybeGrowable DefaultList list2 = new DefaultList<>(); + + void test2(DefaultList tests) { + tests.add("a"); + } + + void foo(@UnmodifiableParam DefaultList list) {} +} From c4e5f76d13c8039e13a845a785816eb870f27900 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Thu, 11 Jun 2026 16:23:07 -0700 Subject: [PATCH 251/308] answer a todo about overriding rule. --- .../ModifiabilityBaseVisitor.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 160f9350bccc..2b12f4a020f4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -145,8 +145,26 @@ private AnnotationMirror positiveCapability() { *

The framework's ordinary receiver override rule allows an overriding method to relax * receiver preconditions. For modifiability operations, that would allow a subtype method to drop * a required {@code @Growable}, {@code @Shrinkable}, or {@code @Replaceable} receiver capability. - * TODO: I don't understand why that's bad? Is it because the checker can't verify collection - * implementations. + * + *

For example: + * + *

{@code
+   * class List {
+   *   void add(@Growable List list) {
+   *     // ...
+   *   }
+   * }
+   *
+   * class A extends List {
+   *   @Override
+   *   void add() {
+   *     throw new UnsupportedOperationException();
+   *   }
+   * }
+   * }
+ * + *

Without requiring the override to preserve the {@code @Growable} receiver, {@code A.add()} + * would be permitted even when {@code A} is only {@code @MaybeMod}. */ @Override protected boolean checkOverride( From b7db6b6179ed1831c780fca5a02f3ea17d806a5d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 11 Jun 2026 18:23:38 -0700 Subject: [PATCH 252/308] Modifiability annotations --- .../annotator/find/ConstructorInsertion.java | 6 ++- .../afu/annotator/find/Criteria.java | 4 +- .../afu/annotator/find/Insertion.java | 3 +- .../afu/annotator/find/TypedInsertion.java | 10 +++-- .../specification/IndexFileSpecification.java | 6 ++- .../afu/scenelib/Annotation.java | 6 ++- .../afu/scenelib/el/AClass.java | 37 +++++++++++-------- .../afu/scenelib/el/ADeclaration.java | 5 ++- .../afu/scenelib/el/AElement.java | 10 +++-- .../afu/scenelib/el/AField.java | 3 +- .../afu/scenelib/el/AScene.java | 7 +++- .../afu/scenelib/el/ATypeElement.java | 3 +- .../afu/scenelib/type/Type.java | 6 ++- .../afu/scenelib/util/coll/VivifyingMap.java | 4 +- .../afu/scenelib/util/coll/WrapperMap.java | 27 ++++++++------ .../formatter/qual/ConversionCategory.java | 3 +- .../qual/I18nConversionCategory.java | 3 +- .../guieffect/GuiEffectTypeFactory.java | 5 ++- .../dataflow/busyexpr/BusyExprStore.java | 5 ++- 19 files changed, 99 insertions(+), 54 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java index 5c92f4698944..7a5e6e3cf151 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java @@ -4,10 +4,11 @@ import java.util.List; import java.util.Set; import org.checkerframework.afu.scenelib.type.Type; +import org.checkerframework.checker.modifiability.qual.Growable; public class ConstructorInsertion extends TypedInsertion { private ReceiverInsertion receiverInsertion = null; - private Set declarationInsertions = new LinkedHashSet<>(); + private @Growable Set declarationInsertions = new LinkedHashSet<>(); /** * Construct a ConstructorInsertion. @@ -20,7 +21,8 @@ public class ConstructorInsertion extends TypedInsertion { * @param criteria where to insert the text * @param innerTypeInsertions the inner types to go on this constructor */ - public ConstructorInsertion(Type type, Criteria criteria, List innerTypeInsertions) { + public ConstructorInsertion( + Type type, Criteria criteria, @Growable List innerTypeInsertions) { super(type, criteria, true, innerTypeInsertions); } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Criteria.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Criteria.java index 373b4cdbb94c..afa2652b72c9 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Criteria.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Criteria.java @@ -13,6 +13,8 @@ import org.checkerframework.afu.scenelib.io.ASTPath; import org.checkerframework.afu.scenelib.io.DebugWriter; import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; import org.objectweb.asm.TypePath; @@ -28,7 +30,7 @@ public final class Criteria { // NOTE: This does not permit multiple criteria of a given kind. /** The set of criterion objects, indexed by kind. */ - private final Map criteria; + private final @Growable @Replaceable Map criteria; /** Creates a new {@code Criteria} without any {@code Criterion}. */ public Criteria() { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java index 1fafc1a8461b..f702fe91754d 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertion.java @@ -14,6 +14,7 @@ import org.checkerframework.afu.scenelib.type.BoundedType; import org.checkerframework.afu.scenelib.type.DeclaredType; import org.checkerframework.afu.scenelib.type.Type; +import org.checkerframework.checker.modifiability.qual.Growable; import org.objectweb.asm.TypePath; import org.plumelib.util.IPair; import org.plumelib.util.StringsPlume; @@ -46,7 +47,7 @@ public enum Kind { * The package names for the annotations being inserted by this Insertion. This will be empty * unless {@link #getText(boolean)} is called with abbreviate=true. */ - protected Set packageNames; + protected @Growable Set packageNames; /** * Set of annotation names that should always be inserted fully-qualified, even when {@link diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java index 3f1ec9617a48..1a4de48d7d40 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java @@ -5,6 +5,7 @@ import org.checkerframework.afu.scenelib.type.BoundedType; import org.checkerframework.afu.scenelib.type.DeclaredType; import org.checkerframework.afu.scenelib.type.Type; +import org.checkerframework.checker.modifiability.qual.Growable; /** * An insertion that may result in code generation other than just annotations. {@code @@ -28,14 +29,15 @@ public abstract class TypedInsertion extends Insertion { protected boolean annotationsOnly; /** The inner types to go on this insertion. See {@link ReceiverInsertion} for more details. */ - protected List innerTypeInsertions; + protected @Growable List innerTypeInsertions; - public TypedInsertion(Type type, Criteria criteria, List innerTypeInsertions) { + public TypedInsertion( + Type type, Criteria criteria, @Growable List innerTypeInsertions) { this(type, criteria, false, innerTypeInsertions); } public TypedInsertion( - Type type, Criteria criteria, boolean b, List innerTypeInsertions) { + Type type, Criteria criteria, boolean b, @Growable List innerTypeInsertions) { super(criteria, b); this.type = type; this.innerTypeInsertions = innerTypeInsertions; @@ -68,7 +70,7 @@ public Type getType() { * * @return a copy of the inner types */ - public List getInnerTypeInsertions() { + public @Growable List getInnerTypeInsertions() { return innerTypeInsertions; } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/specification/IndexFileSpecification.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/specification/IndexFileSpecification.java index d4787dbbf524..375ede2b4bd4 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/specification/IndexFileSpecification.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/specification/IndexFileSpecification.java @@ -46,6 +46,9 @@ import org.checkerframework.afu.scenelib.type.Type; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; import org.checkerframework.checker.formatter.qual.FormatMethod; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.signature.qual.ClassGetName; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; @@ -61,7 +64,8 @@ public class IndexFileSpecification { LinkedHashMultimap.create(); /** The state of this object. */ - private final List insertions = new ArrayList<>(); + private final @Growable @Shrinkable @IteratorPolyMod List insertions = + new ArrayList<>(); /** An insertion for the default constructor, or null. Is a member of insertions if non-null. */ private ConstructorInsertion constructorInsertion = null; diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/Annotation.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/Annotation.java index 0de004c33c40..b2a4b5775f18 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/Annotation.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/Annotation.java @@ -13,6 +13,8 @@ import java.util.StringJoiner; import org.checkerframework.afu.scenelib.el.AnnotationDef; import org.checkerframework.afu.scenelib.field.AnnotationFieldType; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.NonNull; @@ -112,7 +114,9 @@ public Annotation(AnnotationDef def, Map fields) { * @param ja the java.lang.annotation.Annotation to make an Annotation for * @param adefs a cache from which to look up (or insert into) AnnotationDefs */ - public Annotation(java.lang.annotation.Annotation ja, Map adefs) { + // TODO: `adefs` need not be `@Replaceable`. Use the Map Key checker to avoid a warning. + public Annotation( + java.lang.annotation.Annotation ja, @Growable @Replaceable Map adefs) { Class jaType = ja.annotationType(); String name = jaType.getName(); if (adefs.containsKey(name)) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java index 37bafe450c99..a5b66dc0fca8 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java @@ -12,17 +12,20 @@ import javax.lang.model.element.VariableElement; import org.checkerframework.afu.scenelib.Annotation; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; import org.plumelib.util.MapsP; /** An annotated class. */ public class AClass extends ADeclaration { /** The class's annotated type parameter bounds. */ - public final VivifyingMap bounds = + public final @Growable @Replaceable VivifyingMap bounds = ATypeElement.newVivifyingLHMap_ATE(); // -1 maps to superclass, non-negative integers map to implemented interfaces - public final VivifyingMap extendsImplements = - ATypeElement.newVivifyingLHMap_ATE(); + public final @Growable @Replaceable VivifyingMap + extendsImplements = ATypeElement.newVivifyingLHMap_ATE(); /** * The class's annotated methods; a method's key consists of its name followed by its erased @@ -30,16 +33,17 @@ public class AClass extends ADeclaration { * The annotation scene library does not validate the keys, nor does it check that annotated * subelements of the {@link AMethod}s exist in the signature. */ - public final VivifyingMap methods = createMethodMap(); + public final @Growable VivifyingMap methods = createMethodMap(); - public final VivifyingMap staticInits = createInitBlockMap(); + public final @Growable VivifyingMap staticInits = createInitBlockMap(); - public final VivifyingMap instanceInits = createInitBlockMap(); + public final @Growable VivifyingMap instanceInits = createInitBlockMap(); /** The class's annotated fields; map key is field name. */ - public final VivifyingMap fields = AField.newVivifyingLHMap_AF(); + public final @Growable VivifyingMap fields = + AField.newVivifyingLHMap_AF(); - public final VivifyingMap fieldInits = createFieldInitMap(); + public final @Growable VivifyingMap fieldInits = createFieldInitMap(); /** * The type element representing the class. Clients must call {@link #setTypeElement(TypeElement)} @@ -51,7 +55,7 @@ public class AClass extends ADeclaration { public final String className; /** The simple class names any of this class's outer classes (or this class) that are enums. */ - private final HashSet enums = new HashSet<>(); + private final @Growable HashSet enums = new HashSet<>(); /** The enum constants of this class, or null if this class is not an enum. */ private /*@MonotonicNonNull*/ List enumConstants = null; @@ -59,15 +63,15 @@ public class AClass extends ADeclaration { /** * The simple class names any of this class's outer classes (or this class) that are annotations. */ - private final HashSet annotationTypes = new HashSet<>(); + private final @Growable HashSet annotationTypes = new HashSet<>(); /** * The simple class names any of this class's outer classes (or this class) that are interfaces. */ - private final HashSet interfaces = new HashSet<>(); + private final @Growable HashSet interfaces = new HashSet<>(); /** The simple class names any of this class's outer classes (or this class) that are records. */ - private final HashSet records = new HashSet<>(); + private final @Growable HashSet records = new HashSet<>(); // debug fields to keep track of all classes created // private static List debugAllClasses = new ArrayList<>(); @@ -218,7 +222,8 @@ public R accept(ElementVisitor v, T t) { // Static methods - private static VivifyingMap createMethodMap() { + @SuppressWarnings("modifiability:return") // TODO + private static @Modifiable VivifyingMap createMethodMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AMethod createValueFor(String k) { @@ -232,7 +237,8 @@ public boolean isEmptyValue(AMethod v) { }; } - private static VivifyingMap createInitBlockMap() { + @SuppressWarnings("modifiability:return") // TODO + private static @Modifiable VivifyingMap createInitBlockMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override public ABlock createValueFor(Integer k) { @@ -246,7 +252,8 @@ public boolean isEmptyValue(ABlock v) { }; } - private static VivifyingMap createFieldInitMap() { + @SuppressWarnings("modifiability:return") // TODO + private static @Modifiable VivifyingMap createFieldInitMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AExpression createValueFor(String k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java index 77e3c9958f51..4f6c3a73c3f0 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java @@ -4,13 +4,14 @@ import java.util.TreeMap; import org.checkerframework.afu.scenelib.io.ASTPath; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** A declaration, as opposed to an expression. Base class for AClass, AMethod, and AField. */ public abstract class ADeclaration extends AElement { /** * The element's insert-annotation invocations; map key is the AST path to the insertion place. */ - public final VivifyingMap insertAnnotations = + public final @Modifiable VivifyingMap insertAnnotations = new VivifyingMap<>(new TreeMap<>()) { @Override public ATypeElement createValueFor(ASTPath k) { @@ -27,7 +28,7 @@ public boolean isEmptyValue(ATypeElement v) { * The element's annotated insert-typecast invocations; map key is the AST path to the insertion * place */ - public final VivifyingMap insertTypecasts = + public final @Modifiable VivifyingMap insertTypecasts = new VivifyingMap<>(new TreeMap<>()) { @Override public ATypeElementWithType createValueFor(ASTPath k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java index 0a955bc1a6ed..ccf44b45779e 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java @@ -6,6 +6,9 @@ import java.util.StringJoiner; import org.checkerframework.afu.scenelib.Annotation; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; /** * An {@code AElement} represents a Java element and the annotations it carries. Some {@code @@ -21,7 +24,7 @@ public class AElement implements Cloneable { * The top-level annotations directly on this element. Annotations on subelements are in those * subelements' {@code tlAnnotationsHere} sets, not here. */ - public final Set tlAnnotationsHere; + public final @Modifiable Set tlAnnotationsHere; // TODO: What about methods; is `type` the return type? /** @@ -195,7 +198,8 @@ public R accept(ElementVisitor v, T t) { // Static methods - static VivifyingMap newVivifyingLHMap_AE() { + @SuppressWarnings("modifiability:return") + static @Modifiable VivifyingMap newVivifyingLHMap_AE() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AElement createValueFor(K k) { @@ -227,7 +231,7 @@ public boolean isEmptyValue(AElement v) { @SuppressWarnings("unchecked") static void copyMapContents( - VivifyingMap orig, VivifyingMap copy) { + VivifyingMap orig, @Growable @Replaceable VivifyingMap copy) { for (K key : orig.keySet()) { V val = orig.get(key); copy.put(key, (V) val.clone()); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java index 5772a67c5a94..29b8e78affbd 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java @@ -4,6 +4,7 @@ import java.util.regex.Pattern; import javax.lang.model.type.TypeMirror; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** A field or method formal parameter. */ public class AField extends ADeclaration { @@ -154,7 +155,7 @@ public R accept(ElementVisitor v, T t) { return v.visitField(this, t); } - static VivifyingMap newVivifyingLHMap_AF() { + static @Modifiable VivifyingMap newVivifyingLHMap_AF() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AField createValueFor(K k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java index 4eac7c990297..fb2b366bc58f 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java @@ -7,6 +7,7 @@ import java.util.Set; import org.checkerframework.afu.scenelib.io.IndexFileParser; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** * An {@code AScene} (annotated scene) represents the annotations on a set of Java classes and @@ -44,7 +45,8 @@ public class AScene implements Cloneable { private static boolean checkClones = true; /** This scene's annotated packages; map key is package name. */ - public final VivifyingMap packages = AElement.newVivifyingLHMap_AE(); + public final @Modifiable VivifyingMap packages = + AElement.newVivifyingLHMap_AE(); /** * Contains for each annotation type a set of imports to be added to the source if the annotation @@ -56,7 +58,8 @@ public class AScene implements Cloneable { public final Map> imports = new LinkedHashMap<>(); /** This scene's annotated classes; map key is class name. */ - public final VivifyingMap classes = + @SuppressWarnings("modifiability:assignment") // TODO + public final @Modifiable VivifyingMap classes = new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AClass createValueFor(String k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java index ddf80444e25c..8e1f98ae9113 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java @@ -5,6 +5,7 @@ import java.util.Map; import org.checkerframework.afu.scenelib.Annotation; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** * An {@link AElement} that represents a type might have annotations on inner types ("generic/array" @@ -110,7 +111,7 @@ public R accept(ElementVisitor v, T t) { return v.visitTypeElement(this, t); } - static VivifyingMap newVivifyingLHMap_ATE() { + static @Modifiable VivifyingMap newVivifyingLHMap_ATE() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override public ATypeElement createValueFor(K k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/type/Type.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/type/Type.java index 9e432319d5a2..a34633012ae8 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/type/Type.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/type/Type.java @@ -2,6 +2,8 @@ import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** * A representation of a Java type. Handles type parameters, bounded types, arrays and inner types. @@ -17,7 +19,7 @@ public enum Kind { } /** The annotations on the outer type. Empty if there are none. */ - private List annotations; + private @Modifiable @IteratorPolyMod List annotations; /** Constructs a new type with no outer annotations. */ public Type() { @@ -38,7 +40,7 @@ public void addAnnotation(String annotation) { * * @param annotations the new annotations to be placed on this type */ - public void setAnnotations(List annotations) { + public void setAnnotations(@Modifiable @IteratorPolyMod List annotations) { this.annotations = annotations; } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index 90be76c01827..aee3c86d96c7 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -2,6 +2,7 @@ import java.util.Iterator; import java.util.Map; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; /** @@ -24,7 +25,7 @@ public abstract class VivifyingMap extends WrapperMap { * access the backing map directly; the parameter is given mainly so you can provide a new map of * your favorite class ({@link java.util.HashMap}, {@link java.util.LinkedHashMap}, etc.). */ - public VivifyingMap(Map back) { + public @PolyModifiable VivifyingMap(@PolyModifiable Map back) { super(back); } @@ -50,6 +51,7 @@ public V getVivify(K k) { protected abstract V createValueFor(K k); /** Prunes this map by deleting entries with empty values. */ + @SuppressWarnings("modifiability:assignment") // TODO public void prune() { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 5db4a0be9ded..712a50955c21 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,8 +3,12 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.PolyShrinkable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * A {@link WrapperMap} is a map all of whose methods delegate by default to those of a supplied @@ -16,15 +20,15 @@ */ public class WrapperMap implements Map { /** The backing map. */ - protected final Map back; + protected final @Modifiable Map back; /** Constructs a new {@link WrapperMap} with the given backing map. */ - protected WrapperMap(Map back) { + protected WrapperMap(@Modifiable Map back) { this.back = back; } @Override - public void clear() { + public void clear(@Shrinkable WrapperMap this) { back.clear(); } @@ -40,8 +44,7 @@ public boolean containsValue(Object value) { @SuppressWarnings("keyfor") // use of delegate object @Override - public @IteratorPolyMod @PolyShrinkable Set> entrySet( - @PolyShrinkable WrapperMap this) { + public @PolyModifiable Set> entrySet(@PolyModifiable WrapperMap this) { return back.entrySet(); } @@ -56,22 +59,23 @@ public boolean isEmpty() { } @Override - public Set keySet() { + public @PolyModifiable Set keySet(@PolyModifiable WrapperMap this) { return back.keySet(); } @Override - public V put(K key, V value) { + public V put(@Growable @Replaceable WrapperMap this, K key, V value) { return back.put(key, value); } @Override - public void putAll(Map m) { + public void putAll( + @Growable @Replaceable WrapperMap this, Map m) { back.putAll(m); } @Override - public V remove(Object key) { + public V remove(@Shrinkable WrapperMap this, Object key) { return back.remove(key); } @@ -81,7 +85,8 @@ public int size() { } @Override - public Collection values() { + public @IteratorPolyMod @PolyModifiable Collection values( + @PolyModifiable WrapperMap this) { return back.values(); } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java index 0dabb92c2d23..ebbb00162814 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; +import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.framework.qual.AnnotatedFor; @@ -211,7 +212,7 @@ public static ConversionCategory fromConversionChar(char c) { throw new IllegalArgumentException("Bad conversion character " + c); } - private static Set arrayToSet(E[] a) { + private static @Modifiable Set arrayToSet(E[] a) { return new HashSet<>(Arrays.asList(a)); } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java index 02da76a4c901..8a497c8f0eee 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java @@ -5,6 +5,7 @@ import java.util.HashSet; import java.util.Set; import java.util.StringJoiner; +import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.AnnotatedFor; @@ -89,7 +90,7 @@ public static I18nConversionCategory stringToI18nConversionCategory(String strin throw new IllegalArgumentException("Invalid format type " + string); } - private static Set arrayToSet(E[] a) { + private static @Modifiable Set arrayToSet(E[] a) { return new HashSet<>(Arrays.asList(a)); } diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java index 41731423b106..217a25be1999 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java @@ -31,6 +31,7 @@ import org.checkerframework.checker.guieffect.qual.UIEffect; import org.checkerframework.checker.guieffect.qual.UIPackage; import org.checkerframework.checker.guieffect.qual.UIType; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -60,7 +61,7 @@ public class GuiEffectTypeFactory extends BaseAnnotatedTypeFactory { * uses this set and the type annotations of the functional interface of the lambda to figure out * if it can affect the UI or not. */ - protected final Set uiLambdas = new HashSet<>(); + protected final @Growable Set uiLambdas = new HashSet<>(); /** * Keeps track of all anonymous inner classes with inferred UIEffect. @@ -71,7 +72,7 @@ public class GuiEffectTypeFactory extends BaseAnnotatedTypeFactory { * #getAnnotatedType(Tree) getAnnotatedType} will treat this inner class as if it had been * annotated with @UI. */ - protected final Set uiAnonClasses = new HashSet<>(); + protected final @Growable Set uiAnonClasses = new HashSet<>(); /** The @{@link AlwaysSafe} annotation. */ protected final AnnotationMirror ALWAYSSAFE = diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java index c01598810820..0502fe0da4d1 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java @@ -3,6 +3,7 @@ import java.util.LinkedHashSet; import java.util.Set; import java.util.StringJoiner; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; @@ -15,7 +16,7 @@ public class BusyExprStore implements Store { /** A set of busy expression abstract values. */ - private final Set busyExprValueSet; + private final @Shrinkable Set busyExprValueSet; /** * Create a new BusyExprStore. @@ -23,7 +24,7 @@ public class BusyExprStore implements Store { * @param busyExprValueSet a set of busy expression abstract values. The parameter is captured and * the caller should not retain an alias. */ - public BusyExprStore(Set busyExprValueSet) { + public BusyExprStore(@Shrinkable Set busyExprValueSet) { this.busyExprValueSet = busyExprValueSet; } From 703032386e7c7834473dd86064eaf17864362a5f Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 12 Jun 2026 07:06:57 -0700 Subject: [PATCH 253/308] tweaks. --- .../checker/modifiability/ModifiabilityBaseChecker.java | 3 ++- .../checker/modifiability/ModifiabilityChecker.java | 6 +----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index e97d27168d8e..480b3dfaee36 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -2,6 +2,7 @@ import java.util.Set; import org.checkerframework.checker.modifiability.iterator.IteratorChecker; +import org.checkerframework.checker.modifiability.seqgrow.SeqGrowChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.source.SourceChecker; @@ -23,7 +24,7 @@ protected ModifiabilityBaseChecker() {} @Override protected Set> getImmediateSubcheckerClasses() { Set> checkers = super.getImmediateSubcheckerClasses(); - if (this instanceof IteratorChecker) { + if (this instanceof IteratorChecker || this instanceof SeqGrowChecker) { return checkers; } checkers.add(IteratorChecker.class); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 3a4ad9461091..c105c00aec97 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -36,10 +36,6 @@ public ModifiabilityChecker() {} @Override protected Collection> getSupportedCheckers() { return List.of( - GrowChecker.class, - SeqGrowChecker.class, - ShrinkChecker.class, - ReplaceChecker.class, - IteratorChecker.class); + GrowChecker.class, SeqGrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); } } From ec3a0aeb94a281b4954822e96e9acc6dced4487e Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Sun, 14 Jun 2026 19:36:53 -0700 Subject: [PATCH 254/308] test out annotation.unverified with class inside a class Co-authored-by: Copilot --- .../CustomModifiabilityAnnotationWarning.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index bd8847611fcc..af75aa91360a 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -1,9 +1,12 @@ import java.util.AbstractList; +import java.util.AbstractMap; +import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.ListIterator; import java.util.Map; +import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; @@ -11,7 +14,10 @@ import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolyShrinkable; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; public class CustomModifiabilityAnnotationWarning { @@ -62,6 +68,32 @@ public int size() { // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] abstract static @Unmodifiable class ClassLevelUnmodifiableMap implements Map {} + static class MapWithAnnotatedKeySet extends AbstractMap { + @Override + public @Ungrowable @PolyShrinkable @IteratorPolyMod Set< + Map.@PolyModifiable Entry> + entrySet() { + throw new UnsupportedOperationException(); + } + + // :: warning: [modifiability.annotation.unverified] + class KeySet extends AbstractSet { + + // :: error: [super.invocation] + public @Modifiable KeySet() {} + + @Override + public Iterator iterator() { + return java.util.Collections.emptyIterator(); + } + + @Override + public int size() { + return 0; + } + } + } + // :: warning: [modifiability.annotation.unverified] abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator implements Iterator {} From 919849ad149a235f3c80a17f45a3016981ac2405 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 18 Jun 2026 06:29:04 -0700 Subject: [PATCH 255/308] Issue either modifiability.annotation.unverified or super.invocation not both. --- .../ModifiabilityBaseVisitor.java | 126 ++++-------------- .../ClassAnnosVSDefaultQualifierForUse.java | 1 + .../CustomModifiabilityAnnotationWarning.java | 12 +- .../modifiability/SeqGrowableMapTest.java | 3 +- .../tests/modifiability/SeqGrowableTest.java | 3 +- 5 files changed, 31 insertions(+), 114 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 2b12f4a020f4..0d2b980c17cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -1,10 +1,10 @@ package org.checkerframework.checker.modifiability; import com.sun.source.tree.AnnotationTree; -import com.sun.source.tree.ClassTree; +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 com.sun.source.util.TreeScanner; import java.util.ArrayDeque; import java.util.Deque; @@ -14,15 +14,16 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** * Base visitor for the modifiability sub-checkers (Grow, SeqGrow, Shrink, Replace). @@ -37,15 +38,6 @@ public class ModifiabilityBaseVisitor extends BaseTypeVisitor { - /** The erased {@code java.util.Collection} type. */ - private final TypeMirror collectionErasure; - - /** The erased {@code java.util.Map} type. */ - private final TypeMirror mapErasure; - - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - /** Method-scoped {@code @UnmodifiableParam} annotations allowed by their parameter location. */ private final Deque> allowedUnmodifiableParamAnnotations = new ArrayDeque<>(); @@ -60,35 +52,27 @@ public class ModifiabilityBaseVisitor */ public ModifiabilityBaseVisitor(BaseTypeChecker checker) { super(checker); - this.collectionErasure = - atypeFactory.types.erasure( - atypeFactory.getElementUtils().getTypeElement("java.util.Collection").asType()); - this.mapErasure = - atypeFactory.types.erasure( - atypeFactory.getElementUtils().getTypeElement("java.util.Map").asType()); - this.iteratorErasure = - atypeFactory.types.erasure( - atypeFactory.getElementUtils().getTypeElement("java.util.Iterator").asType()); } - /** - * Processes a class declaration and reports an unverified modifiability warning only when a - * source-defined collection-like type explicitly claims a concrete modifiability qualifier on the - * class or one of its constructors. Defaulted/unannotated types and Maybe* annotations do not - * make a verifiable modifiability claim. - * - * @param classTree the class declaration to process - */ @Override - public void processClassTree(ClassTree classTree) { - super.processClassTree(classTree); - if (shouldCheckModifiabilityAnnotationValidity()) { - TypeElement typeElement = TreeUtils.elementFromDeclaration(classTree); - if (typeElement != null - && isCollectionFromSourceCode(typeElement) - && hasExplicitWarningModifiabilityAnnotation(classTree)) { + protected void checkThisOrSuperConstructorCall( + MethodInvocationTree call, @CompilerMessageKey String errorKey) { + + TreePath path = atypeFactory.getPath(call); + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(path); + AnnotatedTypeMirror superType = atypeFactory.getAnnotatedType(call); + AnnotatedExecutableType constructorType = atypeFactory.getAnnotatedType(enclosingMethod); + AnnotatedTypeMirror returnType = constructorType.getReturnType(); + AnnotationMirrorSet topAnnotations = qualHierarchy.getTopAnnotations(); + for (AnnotationMirror topAnno : topAnnotations) { + if (!typeHierarchy.isSubtypeShallowEffective(superType, returnType, topAnno)) { + AnnotationMirror superAnno = superType.getPrimaryAnnotationInHierarchy(topAnno); + AnnotationMirror constructorReturnAnno = + returnType.getPrimaryAnnotationInHierarchy(topAnno); + checker.reportError(call, errorKey, constructorReturnAnno, call, superAnno); + } else if (isWarningModifiabilityAnnotation(returnType.getAnnotationInHierarchy(topAnno))) { checker.reportWarning( - classTree, "modifiability.annotation.unverified", typeElement.getQualifiedName()); + call, "modifiability.annotation.unverified", returnType.getUnderlyingType()); } } } @@ -248,52 +232,6 @@ private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { return annotation != null && atypeFactory.areSameByClass(annotation, UnmodifiableParam.class); } - /** - * Returns true if {@code classTree} explicitly writes a warning-worthy modifiability qualifier on - * the class declaration or on any constructor. - * - * @param classTree a class declaration - * @return true if a warning-worthy modifiability annotation is written on the class or - * constructor - */ - private boolean hasExplicitWarningModifiabilityAnnotation(ClassTree classTree) { - // write explicit modifiability annotations on the class - if (hasWarningModifiabilityAnnotation(classTree.getModifiers().getAnnotations())) { - return true; - } - - // write explicit modifiability annotations on any constructor - for (Tree member : classTree.getMembers()) { - if (member instanceof MethodTree methodTree - && TreeUtils.isConstructor(methodTree) - && hasWarningModifiabilityAnnotation(methodTree.getModifiers().getAnnotations())) { - return true; - } - } - return false; - } - - /** - * Returns true if the list {@code annotations} contains any explicit non-maybe modifiability - * qualifier. - * - *

Iterator through the list of annotations and call - * isWarningModifiablityAnnotation(annotation) - * - * @param annotations annotation trees to inspect - * @return true if one annotation should trigger an unverified modifiability warning - */ - private boolean hasWarningModifiabilityAnnotation( - Iterable annotations) { - for (AnnotationTree annotationTree : annotations) { - AnnotationMirror annotation = TreeUtils.annotationFromAnnotationTree(annotationTree); - if (annotation != null && isWarningModifiabilityAnnotation(annotation)) { - return true; - } - } - return false; - } - /** * Returns true if {@code annotation} is a non-maybe modifiability qualifier that should trigger * the unverified custom modifiability warning. @@ -354,22 +292,4 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} - - /** - * Returns true if {@code typeElement} is a user-defined subtype of {@link java.util.Collection}, - * {@link java.util.Map}, or {@link java.util.Iterator}. - * - * @param typeElement the type element to test - * @return true if {@code typeElement} is a user-defined subtype of Collection, Map, or Iterator - */ - private boolean isCollectionFromSourceCode(TypeElement typeElement) { - if (!ElementUtils.isElementFromSourceCode(typeElement)) { - return false; - } - - TypeMirror type = typeElement.asType(); - return TypesUtils.isErasedSubtype(type, collectionErasure, atypeFactory.types) - || TypesUtils.isErasedSubtype(type, mapErasure, atypeFactory.types) - || TypesUtils.isErasedSubtype(type, iteratorErasure, atypeFactory.types); - } } diff --git a/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java index bd00408f4975..8aee34c5ffd0 100644 --- a/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java +++ b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java @@ -22,6 +22,7 @@ void foo(@UnmodifiableParam ModList list) {} // use @DefaultQualifierForUse @DefaultQualifierForUse(Growable.class) + // :: warning: [modifiability.annotation.unverified] static class DefaultList extends ArrayList {} @MaybeGrowable DefaultList list2 = new DefaultList<>(); diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index af75aa91360a..5b2284a45648 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -44,7 +44,7 @@ abstract static class DirectListIterator implements ListIterator {} static class NonModifiabilityType {} - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + // :: error: [super.invocation] static @Modifiable class ClassLevelModifiableList extends AbstractList { @Override public String get(int index) { @@ -62,10 +62,10 @@ public int size() { // :: warning: [modifiability.annotation.unverified] static @Modifiable class ModifiableSubclassWithoutSuperError extends ArrayList {} - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + // :: error: [super.invocation] abstract static @Growable class ClassLevelGrowableCollection implements Collection {} - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + // :: error: [super.invocation] abstract static @Unmodifiable class ClassLevelUnmodifiableMap implements Map {} static class MapWithAnnotatedKeySet extends AbstractMap { @@ -76,7 +76,6 @@ static class MapWithAnnotatedKeySet extends AbstractMap { throw new UnsupportedOperationException(); } - // :: warning: [modifiability.annotation.unverified] class KeySet extends AbstractSet { // :: error: [super.invocation] @@ -98,7 +97,6 @@ public int size() { abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator implements Iterator {} - // :: warning: [modifiability.annotation.unverified] static class ConstructorLevelShrinkableList extends AbstractList { // :: error: [super.invocation] @Shrinkable ConstructorLevelShrinkableList() {} @@ -161,7 +159,7 @@ public int size() { } } - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + // :: error: [super.invocation] static @Modifiable class SuppressedList extends AbstractList { @Override public String get(int index) { @@ -180,6 +178,6 @@ public boolean add(@IteratorPolyMod SuppressedList this, String e) { } } - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] + // :: error: [super.invocation] abstract static @Unmodifiable class SuppressedMap implements Map {} } diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java index 5dc156ac39f7..5de1e77e3dc6 100644 --- a/checker/tests/modifiability/SeqGrowableMapTest.java +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -16,8 +16,7 @@ public class SeqGrowableMapTest { - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] :: error: - // [declaration.inconsistent.with.implements.clause] + // :: error: [super.invocation] :: error: [declaration.inconsistent.with.implements.clause] abstract static @SeqGrowable class UserDefinedSortedMap implements SortedMap {} void linkedHashMapSupportsSequencedMapGrow() { diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java index f333fa87d0e1..45dcc003708c 100644 --- a/checker/tests/modifiability/SeqGrowableTest.java +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -21,8 +21,7 @@ public class SeqGrowableTest { - // :: warning: [modifiability.annotation.unverified] :: error: [super.invocation] :: - // error:declaration.inconsistent.with.implements.clause + // :: error: [super.invocation] :: error:declaration.inconsistent.with.implements.clause abstract static @SeqGrowable class UserDefinedSortedSet implements SortedSet {} void constructorPositiveCases() { From 2c54a6fa923010e76b9f3277b3146a7b2d0563ef Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 18 Jun 2026 09:29:11 -0700 Subject: [PATCH 256/308] Triggering CI build From 92fba3c1a9e30a59d683ac8bd859b55b72de813e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 18 Jun 2026 10:43:55 -0700 Subject: [PATCH 257/308] more examples for `-AresourceLeakIgnoredExceptions` --- docs/manual/resource-leak-checker.tex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 274199d80fb6..25575a4b6b75 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -575,20 +575,22 @@ \<-AresourceLeakIgnoredExceptions=...>. The option takes a comma-separated list of fully-qualified exception types. A type can be prefixed with \<=> to ignore exactly that type and not its subclasses. For example, for a very pedantic set of ignored -exceptions use: +exceptions use either of these, which have identical effect: \begin{verbatim} - -AresourceLeakIgnoredExceptions=java.lang.Error, =java.lang.NullPointerException + -AresourceLeakIgnoredExceptions="java.lang.Error,=java.lang.NullPointerException" + -AresourceLeakIgnoredExceptions="=java.lang.NullPointerException,java.lang.Error" \end{verbatim} which ignores \ (and all its subclasses) as well as \ (but not its subclasses). The keyword \ will expand to the default set of ignored exceptions. So, -to add an additional exception to the set of ignored exceptions, use: +to add an additional exception to the set of ignored exceptions, use one of these: \begin{verbatim} - -AresourceLeakIgnoredExceptions=default,package.MyCustomException + -AresourceLeakIgnoredExceptions="default,package.MyCustomException" + -AresourceLeakIgnoredExceptions="default,=package.MyCustomException" \end{verbatim} \sectionAndLabel{Errors about field initialization}{resource-leak-field-initialization} From 1b46a3230a9957a241d5bda6a65ace31a603e877 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 18 Jun 2026 11:11:03 -0700 Subject: [PATCH 258/308] Flip if statement. --- .../modifiability/ModifiabilityBaseVisitor.java | 9 +++++---- .../CustomModifiabilityAnnotationWarning.java | 14 +++++++------- .../tests/modifiability/SeqGrowableMapTest.java | 3 ++- checker/tests/modifiability/SeqGrowableTest.java | 3 ++- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 0d2b980c17cf..3ead822992b8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -65,14 +65,15 @@ protected void checkThisOrSuperConstructorCall( AnnotatedTypeMirror returnType = constructorType.getReturnType(); AnnotationMirrorSet topAnnotations = qualHierarchy.getTopAnnotations(); for (AnnotationMirror topAnno : topAnnotations) { - if (!typeHierarchy.isSubtypeShallowEffective(superType, returnType, topAnno)) { + // You coud switch these if statements to change which error is shown. + if (isWarningModifiabilityAnnotation(returnType.getAnnotationInHierarchy(topAnno))) { + checker.reportWarning( + call, "modifiability.annotation.unverified", returnType.getUnderlyingType()); + } else if (!typeHierarchy.isSubtypeShallowEffective(superType, returnType, topAnno)) { AnnotationMirror superAnno = superType.getPrimaryAnnotationInHierarchy(topAnno); AnnotationMirror constructorReturnAnno = returnType.getPrimaryAnnotationInHierarchy(topAnno); checker.reportError(call, errorKey, constructorReturnAnno, call, superAnno); - } else if (isWarningModifiabilityAnnotation(returnType.getAnnotationInHierarchy(topAnno))) { - checker.reportWarning( - call, "modifiability.annotation.unverified", returnType.getUnderlyingType()); } } } diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index 5b2284a45648..a2c74637a5c7 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -44,7 +44,7 @@ abstract static class DirectListIterator implements ListIterator {} static class NonModifiabilityType {} - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] static @Modifiable class ClassLevelModifiableList extends AbstractList { @Override public String get(int index) { @@ -62,10 +62,10 @@ public int size() { // :: warning: [modifiability.annotation.unverified] static @Modifiable class ModifiableSubclassWithoutSuperError extends ArrayList {} - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] abstract static @Growable class ClassLevelGrowableCollection implements Collection {} - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] abstract static @Unmodifiable class ClassLevelUnmodifiableMap implements Map {} static class MapWithAnnotatedKeySet extends AbstractMap { @@ -78,7 +78,7 @@ static class MapWithAnnotatedKeySet extends AbstractMap { class KeySet extends AbstractSet { - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] public @Modifiable KeySet() {} @Override @@ -98,7 +98,7 @@ public int size() { implements Iterator {} static class ConstructorLevelShrinkableList extends AbstractList { - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified]] @Shrinkable ConstructorLevelShrinkableList() {} @Override @@ -159,7 +159,7 @@ public int size() { } } - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] static @Modifiable class SuppressedList extends AbstractList { @Override public String get(int index) { @@ -178,6 +178,6 @@ public boolean add(@IteratorPolyMod SuppressedList this, String e) { } } - // :: error: [super.invocation] + // :: warning: [modifiability.annotation.unverified] abstract static @Unmodifiable class SuppressedMap implements Map {} } diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java index 5de1e77e3dc6..acf7c63be3e9 100644 --- a/checker/tests/modifiability/SeqGrowableMapTest.java +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -16,7 +16,8 @@ public class SeqGrowableMapTest { - // :: error: [super.invocation] :: error: [declaration.inconsistent.with.implements.clause] + // :: warning: [modifiability.annotation.unverified] :: error: + // [declaration.inconsistent.with.implements.clause] abstract static @SeqGrowable class UserDefinedSortedMap implements SortedMap {} void linkedHashMapSupportsSequencedMapGrow() { diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java index 45dcc003708c..9dfdb68fa029 100644 --- a/checker/tests/modifiability/SeqGrowableTest.java +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -21,7 +21,8 @@ public class SeqGrowableTest { - // :: error: [super.invocation] :: error:declaration.inconsistent.with.implements.clause + // :: warning: [modifiability.annotation.unverified] :: error: + // declaration.inconsistent.with.implements.clause abstract static @SeqGrowable class UserDefinedSortedSet implements SortedSet {} void constructorPositiveCases() { From 7ff4f65988d37e36374a6a8d2587ad6cc22024f5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 20 Jun 2026 11:09:15 -0700 Subject: [PATCH 259/308] Use modifiability-annotated plume-util --- annotation-file-utilities/build.gradle | 6 +-- build.gradle | 20 +++++----- checker/build.gradle | 40 +++++++++---------- dataflow/build.gradle | 8 ++-- framework/build.gradle | 2 +- .../stub/RemoveAnnotationsForInference.java | 1 + gradle-mvn-push.gradle | 6 +-- gradle/libs.versions.toml | 2 +- release.gradle | 6 +-- 9 files changed, 46 insertions(+), 45 deletions(-) diff --git a/annotation-file-utilities/build.gradle b/annotation-file-utilities/build.gradle index 11588118a90a..0745f3c6b1ce 100644 --- a/annotation-file-utilities/build.gradle +++ b/annotation-file-utilities/build.gradle @@ -111,9 +111,9 @@ test.dependsOn(testAnnotator) tasks.register("updateUserOptions") { dependsOn( - "updateUserOptionsJavadoc", - "updateUserOptionsHtml" - ) + "updateUserOptionsJavadoc", + "updateUserOptionsHtml" + ) } tasks.register("updateUserOptionsJavadoc", Javadoc) { diff --git a/build.gradle b/build.gradle index 1c81474ad440..946242e0421e 100644 --- a/build.gradle +++ b/build.gradle @@ -98,9 +98,9 @@ tasks.register("installGitHooks", Copy) { } subprojects { subProject -> if (subProject.name.equals("checker") - || subProject.name.equals("annotation-file-utilities") - || subProject.name.equals("dataflow") - || subProject.name.equals("framework")) { + || subProject.name.equals("annotation-file-utilities") + || subProject.name.equals("dataflow") + || subProject.name.equals("framework")) { apply plugin: "com.gradleup.shadow" shadow { addShadowVariantIntoJavaComponent = false @@ -536,13 +536,13 @@ tasks.register("zip", Zip) { from(layout.projectDirectory) { exclude("api/**", - "checker/bin/README", - "checker/dist/*.asc", - "docs/tutorial/tests/**", - "docs/tutorial/src/**", - "docs/tutorial/test/**", - "docs/tutorial/Makefile", - "docs/tutorial/README") + "checker/bin/README", + "checker/dist/*.asc", + "docs/tutorial/tests/**", + "docs/tutorial/src/**", + "docs/tutorial/test/**", + "docs/tutorial/Makefile", + "docs/tutorial/README") include("README.html", "LICENSE.txt", diff --git a/checker/build.gradle b/checker/build.gradle index 701ab24c525b..637f7aae8aba 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -491,10 +491,10 @@ void copyNonannotatedToAnnotatedDirectory(String testdir) { into(file("tests/${testdir}/annotated")) filter { String line -> (line.contains("// :: error:") - // Don't remove unchecked cast warnings, because they're genuinely expected in some all-systems - // tests, such as GenericsCasts.java. - || (line.contains("// :: warning:") && !line.contains("// :: warning: [unchecked]"))) - ? null : line + // Don't remove unchecked cast warnings, because they're genuinely expected in some all-systems + // tests, such as GenericsCasts.java. + || (line.contains("// :: warning:") && !line.contains("// :: warning: [unchecked]"))) + ? null : line } } // The only file for which expected errors are maintained is ExpectedErrors.java, so we copy it over. @@ -950,22 +950,22 @@ tasks.register("wpiManyTest", Exec) { if ( // Ignore the line that WPI echoes with the javac command being run. line.startsWith("Running ") - // Warnings about bad path elements aren't related to WPI and are ignored. - || line.startsWith("warning: [path]") - // Ignore bootstrap classpath warning: - || line.startsWith("warning: [options] bootstrap") - // Ignore the warnings about --add-opens arguments to the JVM - || line.contains("warning: [options] --add-opens has no effect at compile time") - // Ignore warning about using -source; should use --release instead. - || line.equals("warning: [options] system modules path not set in conjunction with -source 11") - // Ignore the summary line that reports the total number of warnings (which can be single or plural). - || line.endsWith(" warning") - || line.endsWith(" warnings") - || line.startsWith("warning: No processor claimed any of these annotations: ") - // Ignore debugging output with the prefix "DEBUG: ". - || line.startsWith("DEBUG: ") - || line.startsWith("warning: DEBUG: ") - || line.contains("warning: [DEBUG: ")) { + // Warnings about bad path elements aren't related to WPI and are ignored. + || line.startsWith("warning: [path]") + // Ignore bootstrap classpath warning: + || line.startsWith("warning: [options] bootstrap") + // Ignore the warnings about --add-opens arguments to the JVM + || line.contains("warning: [options] --add-opens has no effect at compile time") + // Ignore warning about using -source; should use --release instead. + || line.equals("warning: [options] system modules path not set in conjunction with -source 11") + // Ignore the summary line that reports the total number of warnings (which can be single or plural). + || line.endsWith(" warning") + || line.endsWith(" warnings") + || line.startsWith("warning: No processor claimed any of these annotations: ") + // Ignore debugging output with the prefix "DEBUG: ". + || line.startsWith("DEBUG: ") + || line.startsWith("warning: DEBUG: ") + || line.contains("warning: [DEBUG: ")) { return } if (!line.trim().equals("")) { diff --git a/dataflow/build.gradle b/dataflow/build.gradle index 706d7ff2b237..0d1fa0cc998a 100644 --- a/dataflow/build.gradle +++ b/dataflow/build.gradle @@ -221,10 +221,10 @@ final dataflowShadedPom(MavenPublication publication, String shadedPkgName) { // This should just happen automatically, but I can't figure out how to make it happen with // a custom Shadow task. asNode().appendNode("dependencies").appendNode("dependency") - .appendNode("groupId", "org.checkerframework").parent() - .appendNode("artifactId", "checker-qual").parent() - .appendNode("version", version).parent() - .appendNode("scope", "runtime") + .appendNode("groupId", "org.checkerframework").parent() + .appendNode("artifactId", "checker-qual").parent() + .appendNode("version", version).parent() + .appendNode("scope", "runtime") } } } diff --git a/framework/build.gradle b/framework/build.gradle index 662b905a5a86..ad68cbee72c4 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -89,7 +89,7 @@ tasks.register("copyAndMinimizeAnnotatedJdkFiles", JavaExec) { // Populate `annotatedForFiles`. tree.visit { FileVisitDetails fvd -> if (!fvd.file.isDirectory() && fvd.file.name.matches(".*\\.java") - && !fvd.file.path.contains("org/checkerframework")) { + && !fvd.file.path.contains("org/checkerframework")) { fvd.getFile().readLines().any { line -> if (line.contains("@AnnotatedFor") || line.contains("org.checkerframework")) { annotatedForFiles.add(fvd.file.absolutePath) diff --git a/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java b/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java index e1fab19ea8b1..c515af4e15c4 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java @@ -292,6 +292,7 @@ static void removeAnnotations(Path absolutePath, List removals) * @param lineno the index of the line to be removed or replaced * @param newLine the new line for index {@code lineno} */ + @SuppressWarnings("deprecation") // JDK's String.isBlank() only exists on JDK 11+. static void replaceLine(List lines, int lineno, String newLine) { if (StringsPlume.isBlank(newLine)) { lines.remove(lineno); diff --git a/gradle-mvn-push.gradle b/gradle-mvn-push.gradle index dd662a5ab47e..0404fce020d8 100644 --- a/gradle-mvn-push.gradle +++ b/gradle-mvn-push.gradle @@ -7,9 +7,9 @@ publishing { repositories { maven { url = (isSnapshot - ? project.properties.getOrDefault("SNAPSHOT_REPOSITORY_URL", "https://central.sonatype.com/repository/maven-snapshots/") - : project.properties.getOrDefault("RELEASE_REPOSITORY_URL", "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/") - ) + ? project.properties.getOrDefault("SNAPSHOT_REPOSITORY_URL", "https://central.sonatype.com/repository/maven-snapshots/") + : project.properties.getOrDefault("RELEASE_REPOSITORY_URL", "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/") + ) credentials { username = project.properties.get("SONATYPE_NEXUS_USERNAME") password = project.properties.get("SONATYPE_NEXUS_PASSWORD") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c57afae984d5..07fb0ad430dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,7 +74,7 @@ lombok = "org.projectlombok:lombok:1.18.46" options = "org.plumelib:options:2.0.3" -plume-util = "org.plumelib:plume-util:1.14.0" +plume-util = "org.plumelib:plume-util:1.15.0-SNAPSHOT" reflection-util = "org.plumelib:reflection-util:1.1.6" diff --git a/release.gradle b/release.gradle index ab0f080849a5..2354b191bc90 100644 --- a/release.gradle +++ b/release.gradle @@ -54,9 +54,9 @@ tasks.register("updateVersionNumbersInDocs", Copy) { from("docs/") { include("annotation-file-utilities/annotation-file-utilities.html", - "checker-framework-quick-start.html", - "checker-framework-webpage.html", - "manual/introduction.tex") + "checker-framework-quick-start.html", + "checker-framework-webpage.html", + "manual/introduction.tex") filter { String line -> line = line.replaceAll("checker-framework-${releaseVersionRegex}", "checker-framework-${releaseVersion}") From ab972dfc4c146e87144916dbfc6eb95341c8082c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 20 Jun 2026 11:10:23 -0700 Subject: [PATCH 260/308] Use modifiability-annotated plume-util --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5fe2eb842d09..5955b0e54aec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,7 +74,7 @@ lombok = "org.projectlombok:lombok:1.18.46" options = "org.plumelib:options:2.0.3" -plume-util = "org.plumelib:plume-util:1.14.0" +plume-util = "org.plumelib:plume-util:1.15.0-SNAPSHOT" reflection-util = "org.plumelib:reflection-util:1.1.6" From 1f96196d4d2746c4110f4673e9ed2d518479b48b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 27 Jun 2026 21:28:20 -0700 Subject: [PATCH 261/308] New way to verify collection implementations --- .../initialization/InitializationVisitor.java | 2 +- .../checker/interning/InterningVisitor.java | 2 +- ...ModifiabilityBaseAnnotatedTypeFactory.java | 4 + .../ModifiabilityBaseVisitor.java | 249 ++++++++++++++---- .../grow/GrowAnnotatedTypeFactory.java | 1 + .../IteratorAnnotatedTypeFactory.java | 6 + .../checker/modifiability/messages.properties | 7 +- .../replace/ReplaceAnnotatedTypeFactory.java | 1 + .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 1 + .../shrink/ShrinkAnnotatedTypeFactory.java | 1 + .../ClassAnnosVSDefaultQualifierForUse.java | 2 - .../CustomModifiabilityAnnotationWarning.java | 9 - .../ExactReceiverOverrideTest.java | 5 +- .../modifiability/SeqGrowableMapTest.java | 3 +- .../tests/modifiability/SeqGrowableTest.java | 3 +- .../modifiability/UnmodParamLocationTest.java | 2 +- docs/manual/modifiability-checker.tex | 8 +- .../tests/all-systems/GenericsCasts.java | 2 +- .../tests/all-systems/GenericsEnclosing.java | 2 +- .../tests/all-systems/RawTypeAssignment.java | 2 +- .../all-systems/java8inference/Issue3036.java | 2 +- .../javacutil/AnnotationUtils.java | 2 +- 22 files changed, 235 insertions(+), 81 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java index 8cdcc4b193b5..58e082ebe093 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -87,7 +87,7 @@ public void setRoot(CompilationUnitTree newRoot) { protected void checkConstructorInvocation( AnnotatedDeclaredType dt, AnnotatedExecutableType constructor, NewClassTree src) { // Receiver annotations for constructors are forbidden, therefore no check is necessary. - // TODO: nested constructors can have receivers! + // TODO: Constructors for nested classes can have receivers! } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java index 53902b8d0029..843412d6a48c 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java @@ -335,7 +335,7 @@ private boolean isReferenceEqualityImplementation(MethodTree equalsMethod) { protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { if (constructorElement.getEnclosingElement().getKind() == ElementKind.ENUM) { - // Enums constructor are only called once per enum constant. + // Enum constructors are only called once per enum constant. return; } super.checkConstructorResult(constructorType, constructorElement); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 23e41c1477d4..015a65d840c8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -18,6 +18,10 @@ public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotate /** The {@code @}{@link IteratorPolyMod} qualifier. */ protected final AnnotationMirror ITERATOR_PRESERVE_REMOVE; + /** The top qualifier in the hierarchy. Subclasses must set this after calling {@code super()}. */ + // Non-final so subclasses can set it. + protected AnnotationMirror topAnnotation; + /** * Creates a ModifiabilityBaseAnnotatedTypeFactory. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 3ead822992b8..d65388758ade 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -1,28 +1,37 @@ package org.checkerframework.checker.modifiability; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +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 com.sun.source.util.TreePath; import com.sun.source.util.TreeScanner; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.AnnotationMirrorSet; -import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; /** @@ -41,9 +50,12 @@ public class ModifiabilityBaseVisitor /** Method-scoped {@code @UnmodifiableParam} annotations allowed by their parameter location. */ private final Deque> allowedUnmodifiableParamAnnotations = new ArrayDeque<>(); - /** Package that contains the modifiability type-use qualifiers. */ - private static final String MODIFIABILITY_QUAL_PACKAGE = - "org.checkerframework.checker.modifiability.qual"; + // /** Package that contains the modifiability type-use qualifiers. */ + // private static final String MODIFIABILITY_QUAL_PACKAGE = + // "org.checkerframework.checker.modifiability.qual"; + + /** Classes for which an error has been issued, to avoid issuing multiple errors. */ + private static final Set<@FullyQualifiedName String> classWarned = new HashSet<>(); /** * Create a ModifiabilityBaseVisitor. @@ -57,25 +69,7 @@ public ModifiabilityBaseVisitor(BaseTypeChecker checker) { @Override protected void checkThisOrSuperConstructorCall( MethodInvocationTree call, @CompilerMessageKey String errorKey) { - - TreePath path = atypeFactory.getPath(call); - MethodTree enclosingMethod = TreePathUtil.enclosingMethod(path); - AnnotatedTypeMirror superType = atypeFactory.getAnnotatedType(call); - AnnotatedExecutableType constructorType = atypeFactory.getAnnotatedType(enclosingMethod); - AnnotatedTypeMirror returnType = constructorType.getReturnType(); - AnnotationMirrorSet topAnnotations = qualHierarchy.getTopAnnotations(); - for (AnnotationMirror topAnno : topAnnotations) { - // You coud switch these if statements to change which error is shown. - if (isWarningModifiabilityAnnotation(returnType.getAnnotationInHierarchy(topAnno))) { - checker.reportWarning( - call, "modifiability.annotation.unverified", returnType.getUnderlyingType()); - } else if (!typeHierarchy.isSubtypeShallowEffective(superType, returnType, topAnno)) { - AnnotationMirror superAnno = superType.getPrimaryAnnotationInHierarchy(topAnno); - AnnotationMirror constructorReturnAnno = - returnType.getPrimaryAnnotationInHierarchy(topAnno); - checker.reportError(call, errorKey, constructorReturnAnno, call, superAnno); - } - } + // Nothing to do; it is handled by `processClassTree()`. } /** @@ -110,6 +104,155 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { return super.visitAnnotation(tree, p); } + @Override + public void processClassTree(ClassTree tree) { + super.processClassTree(tree); + processClassConstructors(tree); + } + + private void processClassConstructors(ClassTree tree) { + TypeMirror classTM = TreeUtils.elementFromDeclaration(tree).asType(); + if (!atypeFactory.isRelevant(classTM)) { + return; + } + + // It seems difficult to go from an ExecutableElement to the annotations. + // List constructors = + // ElementFilter.constructorsIn(elem.getEnclosedElements()); + + List methods = new ArrayList<>(); + List constructors = new ArrayList<>(); + for (Tree member : tree.getMembers()) { + if (member instanceof MethodTree mt) { + if (TreeUtils.isConstructor(mt)) { + constructors.add(mt); + } else { + methods.add(mt); + } + } + } + + @FullyQualifiedName String className = classTM.toString(); + boolean thisClassWarned = false; + + AnnotationMirror constructorAnno = null; + for (MethodTree constructor : constructors) { + AnnotatedExecutableType constructorType = atypeFactory.getAnnotatedType(constructor); + AnnotatedTypeMirror returnType = constructorType.getReturnType(); + AnnotationMirror thisResultAnno = + returnType.getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation); + if (constructorAnno == null) { + constructorAnno = thisResultAnno; + } else if (!AnnotationUtils.areSameByName(thisResultAnno, constructorAnno)) { + checker.reportError( + constructor, "inconsistent.constructor.result.type", thisResultAnno, constructorAnno); + classWarned.add(className); + thisClassWarned = true; + } + } + if (constructorAnno == null) { + return; + } + + if (thisClassWarned) { + return; + } + + // There is at least one constructor, and all constructors have the same result type annotation. + // Examine: + // * the implementation of each method + // * the annotation of each inherited method, only if the superclass constructors have + // different type than this class's constructors. + + for (MethodTree method : methods) { + AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); + if (receiverType != null) { + AnnotationMirror receiverAnno = receiverType.getAnnotation(); + checkImplOK(method, receiverAnno, constructorAnno, className); + } + } + + // TODO: check for overridden methods, + } + + /** + * Issues an error if the method body does not conform to the given annotation. + * + * @param method a method declaration + * @param constructorAnno the annotation on the class constructors + * @param className the name of the enclosing class, used only for diagnostic messages + */ + private void checkImplOK( + MethodTree method, + AnnotationMirror receiverAnno, + AnnotationMirror constructorAnno, + String className) { + if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation)) { + return; + } + + String receiverAnnoName = + receiverAnno.getAnnotationType().asElement().getSimpleName().toString(); + + if (receiverAnnoName.startsWith("Bottom")) { + checker.reportError(method, "bottom.annotation.on.receiver"); + classWarned.add(className); + } else if (receiverAnnoName.startsWith("Un")) { + // Nothing to check. + } else if (receiverAnnoName.startsWith("Poly")) { + // Nothing to check? + } else { + // The receiver annotation is positive (does not start with "Un"). + // Behavior now depends on the constructor annotation. + String constructorAnnoName = + constructorAnno.getAnnotationType().asElement().getSimpleName().toString(); + if (constructorAnnoName.startsWith("Un")) { + if (!implIsUOE(method)) { + checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); + classWarned.add(className); + } + } else { + if (implIsUOE(method)) { + checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); + classWarned.add(className); + } + } + } + } + + /** + * Returns true if the method body is exactly {@code throws new + * UnsupportedOperationException(...)}. + * + * @param method a method declaration + * @return true if the method body is exactly {@code throws new + * UnsupportedOperationException(...)} + */ + private boolean implIsUOE(MethodTree method) { + BlockTree body = method.getBody(); + List statements = body.getStatements(); + if (statements.size() != 1) { + return false; + } + StatementTree statement = statements.get(0); + if (!(statement instanceof ThrowTree tt)) { + return false; + } + ExpressionTree exception = tt.getExpression(); + if (!(exception instanceof NewClassTree nct)) { + return false; + } + ExpressionTree identifier = nct.getIdentifier(); + if (identifier instanceof IdentifierTree it) { + // TODO: This can be fooled if a different UnsupportedOperationException is imported. + return it.getName().contentEquals("UnsupportedOperationException"); + } else if (identifier instanceof MemberSelectTree mst) { + // TODO: For efficiency, to avoid call to `toString()`, could walk down the MemberSelectTree. + return mst.toString().equals("java.lang.UnsupportedOperationException"); + } + return false; + } + /** * Returns the positive qualifier for this checker's modifiability hierarchy, such as * {@code @Growable}, {@code @SeqGrowable}, {@code @Shrinkable}, or {@code @Replaceable}. @@ -216,7 +359,7 @@ protected boolean shouldCheckModifiabilityAnnotationValidity() { } /** - * Returns true if {@code tree} is an {@link UnmodifiableParam} annotation. + * Returns true if {@code tree} is an {@code @}{@link UnmodifiableParam} annotation. * * @param tree an annotation tree * @return true if {@code tree} is an {@code @UnmodifiableParam} annotation @@ -233,29 +376,33 @@ private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { return annotation != null && atypeFactory.areSameByClass(annotation, UnmodifiableParam.class); } - /** - * Returns true if {@code annotation} is a non-maybe modifiability qualifier that should trigger - * the unverified custom modifiability warning. - * - * @param annotation an annotation mirror - * @return true if {@code annotation} should trigger a warning - */ - private boolean isWarningModifiabilityAnnotation(AnnotationMirror annotation) { - Element element = annotation.getAnnotationType().asElement(); - if (!(element instanceof TypeElement typeElement)) { - return false; - } - - // Only annotations in the modifiability qualifier package are relevant. - String qualifiedName = typeElement.getQualifiedName().toString(); - if (!qualifiedName.startsWith(MODIFIABILITY_QUAL_PACKAGE + ".")) { - return false; - } - - // Maybe* annotations and @UnmodifiableParam are unknown/top-like, so they do not warn. - String simpleName = typeElement.getSimpleName().toString(); - return !simpleName.startsWith("Maybe") && !simpleName.equals("UnmodifiableParam"); - } + // /** + // * Returns true if {@code annotation} is a non-maybe modifiability qualifier that (if written + // on a + // * constructor result type) should trigger a warning about an unverified collection class + // * implementation. + // * + // * @param annotation an annotation mirror written on a constructor result type + // * @return true if {@code annotation} should trigger a warning + // */ + // private boolean isNonTopModifiabilityAnnotation(AnnotationMirror annotation) { + // Element element = annotation.getAnnotationType().asElement(); + // if (!(element instanceof TypeElement typeElement)) { + // return false; + // } + // + // // Only annotations in the modifiability qualifier package are relevant. + // String qualifiedName = typeElement.getQualifiedName().toString(); + // if (!qualifiedName.startsWith(MODIFIABILITY_QUAL_PACKAGE + ".")) { + // return false; + // } + // + // // @Maybe* annotations and @UnmodifiableParam are the top in the hierarchy, so do not warn. + // String simpleName = typeElement.getSimpleName().toString(); + // boolean isTopQualifier = + // simpleName.startsWith("Maybe") || simpleName.equals("UnmodifiableParam"); + // return !isTopQualifier; + // } /** * Returns all {@code @UnmodifiableParam} annotations written on a formal or receiver parameter's diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 9312dd09df7c..c83b7bf1f218 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -72,6 +72,7 @@ public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.GROWABLE = AnnotationBuilder.fromClass(elements, Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(elements, Ungrowable.class); this.POLY_GROWABLE = AnnotationBuilder.fromClass(elements, PolyGrowable.class); + this.topAnnotation = MAYBE_GROWABLE; postInit(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index 2115bf7b1bb6..67d9d4be105c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -20,6 +20,9 @@ /** The annotated type factory for the {@link IteratorChecker}. */ public class IteratorAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { + /** The {@code @}{@link MaybeIteratorPolyMod} qualifier. */ + private final AnnotationMirror MAYBE_ITERATOR_POLY_MOD; + /** The {@code @}{@link IteratorPolyMod} qualifier. */ private final AnnotationMirror ITERATOR_POLY_MOD; @@ -34,8 +37,11 @@ public class IteratorAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedType @SuppressWarnings("this-escape") public IteratorAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); + this.MAYBE_ITERATOR_POLY_MOD = + AnnotationBuilder.fromClass(elements, MaybeIteratorPolyMod.class); this.ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); this.POLY_ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, PolyIteratorPolyMod.class); + this.topAnnotation = MAYBE_ITERATOR_POLY_MOD; postInit(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties index 296143a011c5..04ffd43db6fd 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -1,2 +1,5 @@ -modifiability.annotation.unverified=Class %s is annotated with a modifiability qualifier, but this checker cannot verify custom collection implementations. -unmodparam.location=@UnmodifiableParam may only be written on a formal parameter or receiver parameter. +unmodparam.location=@UnmodifiableParam may only be written on a formal parameter or receiver parameter +inconsistent.constructor.result.type=Constructor result type %s is inconsistent with a previous constructor with result type %s +bottom.annotation.on.receiver=Do not write the bottom annotation on a receiver +method.implementation.is.uoe=Method implementation should not be `throws new UnsupportedOperationException()` +method.implementation.not.uoe=Method implementation should be `throws new UnsupportedOperationException()` diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index cb9989b02599..89ba3d65ccf7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -87,6 +87,7 @@ public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { this.REPLACEABLE = AnnotationBuilder.fromClass(elements, Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(elements, Unreplaceable.class); this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(elements, PolyReplaceable.class); + this.topAnnotation = MAYBE_REPLACEABLE; postInit(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index c3aab0381848..f3ed9e791119 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -76,6 +76,7 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { this.SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, SeqGrowable.class); this.SEQ_UNGROWABLE = AnnotationBuilder.fromClass(elements, SeqUngrowable.class); this.POLY_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, PolySeqGrowable.class); + this.topAnnotation = MAYBE_SEQ_GROWABLE; postInit(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 6ab93fee870d..31325295089c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -66,6 +66,7 @@ public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { this.SHRINKABLE = AnnotationBuilder.fromClass(elements, Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(elements, Unshrinkable.class); this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(elements, PolyShrinkable.class); + this.topAnnotation = MAYBE_SHRINKABLE; postInit(); } diff --git a/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java index 8aee34c5ffd0..e8f4dfb85e0a 100644 --- a/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java +++ b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java @@ -7,7 +7,6 @@ public class ClassAnnosVSDefaultQualifierForUse { // write annotation on class declaration. - // :: warning: [modifiability.annotation.unverified] static @Growable class ModList extends ArrayList {} // :: error: [annotations.on.use] @@ -22,7 +21,6 @@ void foo(@UnmodifiableParam ModList list) {} // use @DefaultQualifierForUse @DefaultQualifierForUse(Growable.class) - // :: warning: [modifiability.annotation.unverified] static class DefaultList extends ArrayList {} @MaybeGrowable DefaultList list2 = new DefaultList<>(); diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index a2c74637a5c7..915f4c747d78 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -44,7 +44,6 @@ abstract static class DirectListIterator implements ListIterator {} static class NonModifiabilityType {} - // :: warning: [modifiability.annotation.unverified] static @Modifiable class ClassLevelModifiableList extends AbstractList { @Override public String get(int index) { @@ -59,13 +58,10 @@ public int size() { // The implicit super() call is fine because ArrayList's constructor already // returns @Modifiable, but the subclass annotation is still an unverified modifiability claim. - // :: warning: [modifiability.annotation.unverified] static @Modifiable class ModifiableSubclassWithoutSuperError extends ArrayList {} - // :: warning: [modifiability.annotation.unverified] abstract static @Growable class ClassLevelGrowableCollection implements Collection {} - // :: warning: [modifiability.annotation.unverified] abstract static @Unmodifiable class ClassLevelUnmodifiableMap implements Map {} static class MapWithAnnotatedKeySet extends AbstractMap { @@ -78,7 +74,6 @@ static class MapWithAnnotatedKeySet extends AbstractMap { class KeySet extends AbstractSet { - // :: warning: [modifiability.annotation.unverified] public @Modifiable KeySet() {} @Override @@ -93,12 +88,10 @@ public int size() { } } - // :: warning: [modifiability.annotation.unverified] abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator implements Iterator {} static class ConstructorLevelShrinkableList extends AbstractList { - // :: warning: [modifiability.annotation.unverified]] @Shrinkable ConstructorLevelShrinkableList() {} @Override @@ -159,7 +152,6 @@ public int size() { } } - // :: warning: [modifiability.annotation.unverified] static @Modifiable class SuppressedList extends AbstractList { @Override public String get(int index) { @@ -178,6 +170,5 @@ public boolean add(@IteratorPolyMod SuppressedList this, String e) { } } - // :: warning: [modifiability.annotation.unverified] abstract static @Unmodifiable class SuppressedMap implements Map {} } diff --git a/checker/tests/modifiability/ExactReceiverOverrideTest.java b/checker/tests/modifiability/ExactReceiverOverrideTest.java index 265ace54735a..898bebd5b0ce 100644 --- a/checker/tests/modifiability/ExactReceiverOverrideTest.java +++ b/checker/tests/modifiability/ExactReceiverOverrideTest.java @@ -9,7 +9,7 @@ import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; -@SuppressWarnings("modifiability:annotation.unverified") +@SuppressWarnings("class.unverified") public class ExactReceiverOverrideTest { static class GrowParent extends ArrayList { @@ -98,6 +98,9 @@ void update() {} } static class UnmodifiableArrayList extends ArrayList { + + @Unmodifiable UnmodifiableArrayList() {} + @Override public boolean add(@Growable UnmodifiableArrayList this, E element) { throw new UnsupportedOperationException(); diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java index acf7c63be3e9..8e97b2818cda 100644 --- a/checker/tests/modifiability/SeqGrowableMapTest.java +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -16,8 +16,7 @@ public class SeqGrowableMapTest { - // :: warning: [modifiability.annotation.unverified] :: error: - // [declaration.inconsistent.with.implements.clause] + // :: error: [declaration.inconsistent.with.implements.clause] abstract static @SeqGrowable class UserDefinedSortedMap implements SortedMap {} void linkedHashMapSupportsSequencedMapGrow() { diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java index 9dfdb68fa029..a9eccbdf5d91 100644 --- a/checker/tests/modifiability/SeqGrowableTest.java +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -21,8 +21,7 @@ public class SeqGrowableTest { - // :: warning: [modifiability.annotation.unverified] :: error: - // declaration.inconsistent.with.implements.clause + // :: error: declaration.inconsistent.with.implements.clause abstract static @SeqGrowable class UserDefinedSortedSet implements SortedSet {} void constructorPositiveCases() { diff --git a/checker/tests/modifiability/UnmodParamLocationTest.java b/checker/tests/modifiability/UnmodParamLocationTest.java index 973ba8c68496..b5b1b28bd167 100644 --- a/checker/tests/modifiability/UnmodParamLocationTest.java +++ b/checker/tests/modifiability/UnmodParamLocationTest.java @@ -18,7 +18,7 @@ void method(@UnmodifiableParam List parameter) {} void nestedParameter(List<@UnmodifiableParam List> parameter) {} - @SuppressWarnings("modifiability:annotation.unverified") + @SuppressWarnings("class.unverified") abstract static class RelevantReceiver implements List { void receiver(@UnmodifiableParam RelevantReceiver this) {} } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index cad9a5506af7..880e2f45c32f 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -609,7 +609,7 @@ it3.remove(); // Error: list3 does not have @IteratorPolyMod \end{Verbatim} -\sectionAndLabel{Unverified collection implementation annotations}{modifiability-annotation-unverified} +\sectionAndLabel{Unverified collection implementation annotations}{class.unverified} The Modifiability Checker verifies uses of collection modifiability annotations, but it does not verify collection implementations. @@ -617,14 +617,14 @@ When a user-defined collection-like class (a \, \, \, or subtype) explicitly writes a concrete modifiability qualifier on the class declaration or on a constructor, the checker issues a -\ warning. The warning means that the +\ warning. The warning means that the annotation is a trusted specification of the custom implementation, not a fact verified from the implementation's method bodies. After examining the implementation, suppress the warning with -\<@SuppressWarnings("modifiability:annotation.unverified")>. +\<@SuppressWarnings("class.unverified")>. For example, the class-level \<@Growable> annotation below triggers -\. It is up to the programmer to verify that +\. It is up to the programmer to verify that the implementation of \ is not \@Growable and remove it from the class declaration. \begin{Verbatim} public @Growable class TestMod extends ArrayList { diff --git a/framework/tests/all-systems/GenericsCasts.java b/framework/tests/all-systems/GenericsCasts.java index fd382c621b89..44b11a13b914 100644 --- a/framework/tests/all-systems/GenericsCasts.java +++ b/framework/tests/all-systems/GenericsCasts.java @@ -27,7 +27,7 @@ void m() { // are currently problematic. // TODO: try to find a problem with skipping this check. class Test { - @SuppressWarnings("modifiability:annotation.unverified") + @SuppressWarnings("class.unverified") @Modifiable class Entry extends LinkedList {} class Queue { diff --git a/framework/tests/all-systems/GenericsEnclosing.java b/framework/tests/all-systems/GenericsEnclosing.java index 8e809027b260..e5b4bc119448 100644 --- a/framework/tests/all-systems/GenericsEnclosing.java +++ b/framework/tests/all-systems/GenericsEnclosing.java @@ -9,7 +9,7 @@ * *

Also see regex/GenericsEnclosing for a test case for the Regex Checker. */ -@SuppressWarnings("modifiability:annotation.unverified") +@SuppressWarnings("class.unverified") public @Growable @Replaceable @SeqUngrowable class GenericsEnclosing extends TreeMap { class Inner { diff --git a/framework/tests/all-systems/RawTypeAssignment.java b/framework/tests/all-systems/RawTypeAssignment.java index 4f1675b89696..23c3791de84a 100644 --- a/framework/tests/all-systems/RawTypeAssignment.java +++ b/framework/tests/all-systems/RawTypeAssignment.java @@ -4,7 +4,7 @@ class Component {} -@SuppressWarnings("modifiability:annotation.unverified") +@SuppressWarnings("class.unverified") @Modifiable class Components extends ArrayList {} // If we include a type parameter in the superclass, then there is no error below. diff --git a/framework/tests/all-systems/java8inference/Issue3036.java b/framework/tests/all-systems/java8inference/Issue3036.java index 923fe624644e..e5f7e2f3ef4a 100644 --- a/framework/tests/all-systems/java8inference/Issue3036.java +++ b/framework/tests/all-systems/java8inference/Issue3036.java @@ -43,7 +43,7 @@ private void write(Stream stream) { .collect(Collectors.toList()); } - @SuppressWarnings("modifiability:annotation.unverified") + @SuppressWarnings("class.unverified") public static @Modifiable class ImmutableMap extends HashMap { public static ImmutableMap of(K k1, V v1, K k2, V v2) { throw new RuntimeException(); diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java index 800e4c7cd697..f90a6999eab7 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java @@ -1389,7 +1389,7 @@ public static List annotationValueToList( } /** - * Returns the annotations explicitly written on a constructor result. Callers should check that + * Returns the annotations explicitly written on a constructor result. Callers must ensure that * {@code constructorDeclaration} is in fact a declaration of a constructor. * * @param constructorDeclaration declaration tree of constructor From ef7d8db2ea31155f9738a01fa8d2560fefd8e8be Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 2 Jul 2026 10:54:21 -0700 Subject: [PATCH 262/308] Use `@MonotonicNonNull` annotation --- .../checker/lock/LockAnnotatedTypeFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java index 29f08b628814..12aed44ec29b 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java @@ -33,6 +33,7 @@ import org.checkerframework.checker.lock.qual.MayReleaseLocks; import org.checkerframework.checker.lock.qual.NewObject; import org.checkerframework.checker.lock.qual.ReleasesNoLocks; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; @@ -468,7 +469,7 @@ boolean isWeakerThan(SideEffectAnnotation other) { } /** The weakest side effect annotation. */ - static SideEffectAnnotation weakest = null; + static @MonotonicNonNull SideEffectAnnotation weakest; /** * Returns the weakest side effect annotation. From 14e9d961a68a53a387e245dca9477b2750538a57 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 1 Aug 2026 09:39:39 -0700 Subject: [PATCH 263/308] Add newline at end of file --- .../checkerframework/checker/modifiability/grow/ical4j.astub | 2 +- .../org/checkerframework/checker/modifiability/ical4j.astub | 2 +- .../checkerframework/checker/modifiability/replace/ical4j.astub | 2 +- .../checkerframework/checker/modifiability/shrink/ical4j.astub | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub index 8bac550ff5b4..0791ab0f91d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub @@ -4,4 +4,4 @@ package net.fortuna.ical4j.model; @Modifiable class ComponentList {} -@Modifiable class PropertyList {} \ No newline at end of file +@Modifiable class PropertyList {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub index 8bac550ff5b4..0791ab0f91d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub @@ -4,4 +4,4 @@ package net.fortuna.ical4j.model; @Modifiable class ComponentList {} -@Modifiable class PropertyList {} \ No newline at end of file +@Modifiable class PropertyList {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub index 8bac550ff5b4..0791ab0f91d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub @@ -4,4 +4,4 @@ package net.fortuna.ical4j.model; @Modifiable class ComponentList {} -@Modifiable class PropertyList {} \ No newline at end of file +@Modifiable class PropertyList {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub index 8bac550ff5b4..0791ab0f91d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub @@ -4,4 +4,4 @@ package net.fortuna.ical4j.model; @Modifiable class ComponentList {} -@Modifiable class PropertyList {} \ No newline at end of file +@Modifiable class PropertyList {} From 5f121c79ef195d570c0f74a0e9e5c365de21b95e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 1 Aug 2026 12:12:16 -0700 Subject: [PATCH 264/308] Documentation --- .../java/org/checkerframework/afu/scenelib/el/AClass.java | 2 +- .../checker/formatter/qual/ConversionCategory.java | 7 +++++++ .../checker/i18nformatter/qual/I18nConversionCategory.java | 7 +++++++ checker/bin-devel/count-suppression-reasons | 2 +- .../checker/initialization/InitializationVisitor.java | 2 +- .../checker/interning/InterningVisitor.java | 2 +- docs/manual/manual-style.tex | 4 ++-- .../org/checkerframework/javacutil/AnnotationUtils.java | 2 +- 8 files changed, 21 insertions(+), 7 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java index 21c1ff593261..534a452238f9 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java @@ -21,7 +21,7 @@ public class AClass extends ADeclaration { public final VivifyingMap bounds = ATypeElement.newVivifyingLHMap_ATE(); - // -1 maps to superclass, non-negative integers map to implemented interfaces + /** -1 maps to superclass, non-negative integers map to implemented interfaces. */ public final VivifyingMap extendsImplements = ATypeElement.newVivifyingLHMap_ATE(); diff --git a/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java index ee18f00dbc16..9d76da93e24e 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java @@ -211,6 +211,13 @@ public static ConversionCategory fromConversionChar(char c) { throw new IllegalArgumentException("Bad conversion character " + c); } + /** + * Converts an array to a set. + * + * @param a an array + * @param the type of array and set elements + * @return a set containing the array's elements + */ private static Set arrayToSet(E[] a) { return new HashSet<>(Arrays.asList(a)); } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java index d1731c24c053..3d99894418eb 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java @@ -89,6 +89,13 @@ public static I18nConversionCategory stringToI18nConversionCategory(String strin throw new IllegalArgumentException("Invalid format type " + string); } + /** + * Converts an array to a set. + * + * @param a an array + * @param the type of array and set elements + * @return a set containing the array's elements + */ private static Set arrayToSet(E[] a) { return new HashSet<>(Arrays.asList(a)); } diff --git a/checker/bin-devel/count-suppression-reasons b/checker/bin-devel/count-suppression-reasons index becc4e26480f..3d39e87d2d69 100755 --- a/checker/bin-devel/count-suppression-reasons +++ b/checker/bin-devel/count-suppression-reasons @@ -9,7 +9,7 @@ # -d debug # -i REGEX include matches for the grep basic regex # -x REGEX exclude matches for the grep basic regex -# -y REGEX exclude matches for the grep basic regex +# -y REGEX exclude matches for the grep basic regex (enables supplying two regexes) # The -i argument (if any) is processed before the -x argument (if any). # The "reason" for a warning suppression is the Java line comment after it: diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java index 8cdcc4b193b5..58e082ebe093 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -87,7 +87,7 @@ public void setRoot(CompilationUnitTree newRoot) { protected void checkConstructorInvocation( AnnotatedDeclaredType dt, AnnotatedExecutableType constructor, NewClassTree src) { // Receiver annotations for constructors are forbidden, therefore no check is necessary. - // TODO: nested constructors can have receivers! + // TODO: Constructors for nested classes can have receivers! } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java index 53902b8d0029..843412d6a48c 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java @@ -335,7 +335,7 @@ private boolean isReferenceEqualityImplementation(MethodTree equalsMethod) { protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { if (constructorElement.getEnclosingElement().getKind() == ElementKind.ENUM) { - // Enums constructor are only called once per enum constant. + // Enum constructors are only called once per enum constant. return; } super.checkConstructorResult(constructorType, constructorElement); diff --git a/docs/manual/manual-style.tex b/docs/manual/manual-style.tex index 57792ad6801c..a1dd6a5cae4a 100644 --- a/docs/manual/manual-style.tex +++ b/docs/manual/manual-style.tex @@ -171,9 +171,9 @@ \newcommand{\refenum}[4]{\href{../api/org/checkerframework/#1/#2.html\##3#4}{\<#3>}} % Reference to Oracle (previously Sun) Javadoc. -% Arg 1: .html reference, without the .../api/ prefix +% Arg 1: .html reference, without the .../api/ prefix. Quote "#" as "\#" if it appears. % Arg 2: What will appear in the formatted manual. -% Do not use \url in the body of any macro +% Do not use \url in the body of any macro. \newcommand{\sunjavadoc}[2]{\href{https://docs.oracle.com/en/java/javase/25/docs/api/#1}{\<#2>}} % Like \sunjavadoc, but for annotations and prepends "@". \newcommand{\sunjavadocanno}[2]{\href{https://docs.oracle.com/en/java/javase/25/docs/api/#1}{\<@#2>}} diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java index 30c12028e66a..89899771db89 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java @@ -1390,7 +1390,7 @@ public static List annotationValueToList( } /** - * Returns the annotations explicitly written on a constructor result. Callers should check that + * Returns the annotations explicitly written on a constructor result. Callers must ensure that * {@code constructorDeclaration} is in fact a declaration of a constructor. * * @param constructorDeclaration declaration tree of constructor From 534fb6912ecfd4f4c98b3545ad22a66c25ed7bf9 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 1 Aug 2026 12:22:50 -0700 Subject: [PATCH 265/308] Code review --- .../main/java/org/checkerframework/afu/scenelib/el/AClass.java | 2 +- checker/bin-devel/count-suppression-reasons | 3 ++- .../checker/initialization/InitializationVisitor.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java index 534a452238f9..5cd9c9a39162 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java @@ -21,7 +21,7 @@ public class AClass extends ADeclaration { public final VivifyingMap bounds = ATypeElement.newVivifyingLHMap_ATE(); - /** -1 maps to superclass, non-negative integers map to implemented interfaces. */ + /** -1 maps to superclass, non-negative integers map to implemented or extended interfaces. */ public final VivifyingMap extendsImplements = ATypeElement.newVivifyingLHMap_ATE(); diff --git a/checker/bin-devel/count-suppression-reasons b/checker/bin-devel/count-suppression-reasons index 3d39e87d2d69..82ce4442712c 100755 --- a/checker/bin-devel/count-suppression-reasons +++ b/checker/bin-devel/count-suppression-reasons @@ -9,7 +9,8 @@ # -d debug # -i REGEX include matches for the grep basic regex # -x REGEX exclude matches for the grep basic regex -# -y REGEX exclude matches for the grep basic regex (enables supplying two regexes) +# -y REGEX exclude matches for the grep basic regex (use of both -x +# and -y enables supplying two regexes) # The -i argument (if any) is processed before the -x argument (if any). # The "reason" for a warning suppression is the Java line comment after it: diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java index 58e082ebe093..af65f2532e20 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -87,7 +87,7 @@ public void setRoot(CompilationUnitTree newRoot) { protected void checkConstructorInvocation( AnnotatedDeclaredType dt, AnnotatedExecutableType constructor, NewClassTree src) { // Receiver annotations for constructors are forbidden, therefore no check is necessary. - // TODO: Constructors for nested classes can have receivers! + // TODO: Constructors for inner classes can have receivers! } @Override From f92c2f4e9b69e7e08e698d776a54e5ae039b6d03 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 10:32:29 -0700 Subject: [PATCH 266/308] Add Javadoc --- .../checker/modifiability/ModifiabilityBaseVisitor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index d65388758ade..8259bdbb19e3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -110,6 +110,11 @@ public void processClassTree(ClassTree tree) { processClassConstructors(tree); } + /** + * Processes the constructors of a class. + * + * @param tree a class + */ private void processClassConstructors(ClassTree tree) { TypeMirror classTM = TreeUtils.elementFromDeclaration(tree).asType(); if (!atypeFactory.isRelevant(classTM)) { From d3e99b89556e117839a68ec6f35cfee5f64131ce Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 13:16:01 -0700 Subject: [PATCH 267/308] CI diagnostics --- .azure/Makefile | 2 +- .circleci/Makefile | 2 +- .github/workflows/Makefile | 2 +- .github/workflows/ci.yml | 19 ++++--------------- .github/workflows/ci.yml.m4 | 7 ++++--- .github/workflows/defs.m4 | 4 ---- 6 files changed, 11 insertions(+), 25 deletions(-) diff --git a/.azure/Makefile b/.azure/Makefile index 80706e04b985..c9e4639dfe7e 100644 --- a/.azure/Makefile +++ b/.azure/Makefile @@ -14,7 +14,7 @@ PLUME_SCRIPTS=../checker/bin-devel/.plume-scripts # @chmod -w $@ ${PLUME_SCRIPTS}: - make -C .. get-plume-scripts + cd .. && ./gradlew getPlumeScripts clean: @rm -f azure-pipelines.yml azure-pipelines-daily.yml diff --git a/.circleci/Makefile b/.circleci/Makefile index 3baf4e1da5e0..6b2bc7c401ca 100644 --- a/.circleci/Makefile +++ b/.circleci/Makefile @@ -18,7 +18,7 @@ validate: config.yml cd .. && circleci config validate ${PLUME_SCRIPTS}: - make -C .. get-plume-scripts + cd .. && ./gradlew getPlumeScripts clean: @rm -f config.yml diff --git a/.github/workflows/Makefile b/.github/workflows/Makefile index 45469c3f3e0d..8c4a4e855aac 100644 --- a/.github/workflows/Makefile +++ b/.github/workflows/Makefile @@ -14,7 +14,7 @@ PLUME_SCRIPTS=../../checker/bin-devel/.plume-scripts # @chmod -w $@ ${PLUME_SCRIPTS}: - make -C ../.. get-plume-scripts + cd ../.. && ./gradlew getPlumeScripts clean: @rm -f ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96b5bb62f50a..2185a7d78da0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,12 +50,13 @@ jobs: - uses: actions/checkout@v7 with: set-safe-directory: true - # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: clone_plume_scripts run: git clone https://github.com/plume-lib/plume-scripts.git /tmp/plume-scripts - - name: ci_info - run: /tmp/plume-scripts/ci-info --debug + - name: ci_org_and_branch + run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug + - name: git_changes + run: ./checker/bin-devel/.plume-scripts/git-changes --debug junit_jdk17: needs: @@ -223,10 +224,6 @@ jobs: fetch-depth: 0 - name: getPlumeScripts run: ./gradlew -q getPlumeScripts - - name: ci-org-and-branch - run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - name: git-changes - run: ./checker/bin-devel/.plume-scripts/git-changes --debug - name: test-misc.sh run: ./checker/bin-devel/test-misc.sh env: @@ -243,10 +240,6 @@ jobs: fetch-depth: 0 - name: getPlumeScripts run: ./gradlew -q getPlumeScripts - - name: ci-org-and-branch - run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - name: git-changes - run: ./checker/bin-devel/.plume-scripts/git-changes --debug - name: test-misc.sh run: ./checker/bin-devel/test-misc.sh env: @@ -263,10 +256,6 @@ jobs: fetch-depth: 0 - name: getPlumeScripts run: ./gradlew -q getPlumeScripts - - name: ci-org-and-branch - run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - name: git-changes - run: ./checker/bin-devel/.plume-scripts/git-changes --debug - name: test-misc.sh run: ./checker/bin-devel/test-misc.sh env: diff --git a/.github/workflows/ci.yml.m4 b/.github/workflows/ci.yml.m4 index 8e23ca4beb59..546348eb51c2 100644 --- a/.github/workflows/ci.yml.m4 +++ b/.github/workflows/ci.yml.m4 @@ -53,12 +53,13 @@ jobs: - uses: actions/checkout@v7 with: set-safe-directory: true - # Unlimited history for contributors.tex generation. fetch-depth: 0 - name: clone_plume_scripts run: git clone https://github.com/plume-lib/plume-scripts.git /tmp/plume-scripts - - name: ci_info - run: /tmp/plume-scripts/ci-info --debug + - name: ci_org_and_branch + run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug + - name: git_changes + run: ./checker/bin-devel/.plume-scripts/git-changes --debug include([../../.azure/jobs.m4])dnl diff --git a/.github/workflows/defs.m4 b/.github/workflows/defs.m4 index a6f15c52bd69..1c12ed520626 100644 --- a/.github/workflows/defs.m4 +++ b/.github/workflows/defs.m4 @@ -100,10 +100,6 @@ ifelse($1,canary_jdk,,$1,latest_jdk,,[ dependsOn: fetch-depth: 0 - name: getPlumeScripts run: ./gradlew -q getPlumeScripts - - name: ci-org-and-branch - run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - name: git-changes - run: ./checker/bin-devel/.plume-scripts/git-changes --debug - name: test-misc.sh run: ./checker/bin-devel/test-misc.sh env: From 0ca548714eeb2e00dc71e52e46a6f5d133e682a5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 13:59:39 -0700 Subject: [PATCH 268/308] `ci_info` job --- .circleci/config.yml | 48 +++++++++++++++++++++++-------------- .circleci/config.yml.m4 | 30 +++++++++++++++++++++++ .circleci/defs.m4 | 6 ----- .github/workflows/ci.yml | 2 +- .github/workflows/ci.yml.m4 | 2 +- prek.toml | 17 ++++++------- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 024d5b0a8394..6712d70dca43 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,6 +22,34 @@ jobs: steps: - run: /bin/true + ci_info: + docker: + - image: 'mdernst/cf-ubuntu-jdk25-plus' + resource_class: small + environment: + TERM: dumb + steps: + - restore_cache: + keys: + - &sourcefull-cache source-v1full-{{ .Branch }}-{{ .Revision }} + - 'source-v1full-{{ .Branch }}-' + - source-v1full- + - checkout: + method: full + - save_cache: + key: *sourcefull-cache + paths: + - .git + - run: + name: getPlumeScripts + command: ./gradlew -q getPlumeScripts + - run: + name: ci-org-and-branch + command: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug + - run: + name: git-changes + command: ./checker/bin-devel/.plume-scripts/git-changes --debug + junit_jdk17: docker: - image: 'mdernst/cf-ubuntu-jdk17' @@ -258,12 +286,6 @@ jobs: - run: name: getPlumeScripts command: ./gradlew -q getPlumeScripts - - run: - name: ci-org-and-branch - command: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - run: - name: git-changes - command: ./checker/bin-devel/.plume-scripts/git-changes --debug - run: name: test-misc.sh command: ./checker/bin-devel/test-misc.sh @@ -291,12 +313,6 @@ jobs: - run: name: getPlumeScripts command: ./gradlew -q getPlumeScripts - - run: - name: ci-org-and-branch - command: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - run: - name: git-changes - command: ./checker/bin-devel/.plume-scripts/git-changes --debug - run: name: test-misc.sh command: ./checker/bin-devel/test-misc.sh @@ -324,12 +340,6 @@ jobs: - run: name: getPlumeScripts command: ./gradlew -q getPlumeScripts - - run: - name: ci-org-and-branch - command: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - run: - name: git-changes - command: ./checker/bin-devel/.plume-scripts/git-changes --debug - run: name: test-misc.sh command: ./checker/bin-devel/test-misc.sh @@ -471,6 +481,8 @@ workflows: - misc_jdk25 - misc_jdk26 + - ci_info + - junit_part1_jdk25 - junit_part2_jdk25 - nonjunit_jdk25 diff --git a/.circleci/config.yml.m4 b/.circleci/config.yml.m4 index 45ab7c642773..b8ec54ff85fe 100644 --- a/.circleci/config.yml.m4 +++ b/.circleci/config.yml.m4 @@ -26,6 +26,34 @@ jobs: steps: - run: /bin/true + ci_info: + docker: + - image: 'mdernst/cf-ubuntu-jdk25-plus' + resource_class: small + environment: + TERM: dumb + steps: + - restore_cache: + keys: + - &sourcefull-cache source-v1full-{{ .Branch }}-{{ .Revision }} + - 'source-v1full-{{ .Branch }}-' + - source-v1full- + - checkout: + method: full + - save_cache: + key: *sourcefull-cache + paths: + - .git + - run: + name: getPlumeScripts + command: ./gradlew -q getPlumeScripts + - run: + name: ci-org-and-branch + command: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug + - run: + name: git-changes + command: ./checker/bin-devel/.plume-scripts/git-changes --debug + include([../.azure/jobs.m4])dnl # The "workflows" section determines which jobs run and what other jobs they depend on. @@ -45,6 +73,8 @@ workflows: - misc_jdk[]canary_jdk - misc_jdk[]latest_jdk + - ci_info + job_dependences(canary_jdk, junit_part1) job_dependences(canary_jdk, junit_part2) job_dependences(canary_jdk, nonjunit) diff --git a/.circleci/defs.m4 b/.circleci/defs.m4 index b00886ae7a9b..53d136723522 100644 --- a/.circleci/defs.m4 +++ b/.circleci/defs.m4 @@ -104,12 +104,6 @@ circleci_boilerplate($1,-plus,full) - run: name: getPlumeScripts command: ./gradlew -q getPlumeScripts - - run: - name: ci-org-and-branch - command: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - - run: - name: git-changes - command: ./checker/bin-devel/.plume-scripts/git-changes --debug - run: name: test-misc.sh command: ./checker/bin-devel/test-misc.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2185a7d78da0..9d2408deadda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: set-safe-directory: true fetch-depth: 0 - name: clone_plume_scripts - run: git clone https://github.com/plume-lib/plume-scripts.git /tmp/plume-scripts + run: ./gradlew -q getPlumeScripts - name: ci_org_and_branch run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - name: git_changes diff --git a/.github/workflows/ci.yml.m4 b/.github/workflows/ci.yml.m4 index 546348eb51c2..787d77d388bb 100644 --- a/.github/workflows/ci.yml.m4 +++ b/.github/workflows/ci.yml.m4 @@ -55,7 +55,7 @@ jobs: set-safe-directory: true fetch-depth: 0 - name: clone_plume_scripts - run: git clone https://github.com/plume-lib/plume-scripts.git /tmp/plume-scripts + run: ./gradlew -q getPlumeScripts - name: ci_org_and_branch run: ./checker/bin-devel/.plume-scripts/ci-org-and-branch --debug - name: git_changes diff --git a/prek.toml b/prek.toml index 43154d75f7a0..24393a36385e 100644 --- a/prek.toml +++ b/prek.toml @@ -88,14 +88,15 @@ hooks = [ ## CircleCI -# This doesn't install the tool! It fails with "Circleci CLI could not be -# found." unless you have installed the CircleCI CLI. Using `snap` to install -# leads to permission problems due to snap confinement. To install: -# - run: curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/main/install.sh | sudo bash -[[repos]] -repo = "https://github.com/zahorniak/pre-commit-circleci" -rev = "v1.3.0" -hooks = [{ id = "circleci_validate" }] +# # This doesn't install the tool! It fails with "Circleci CLI could not be +# # found." unless you have installed the CircleCI CLI. Using `snap` to install +# # leads to permission problems due to snap confinement. To install: +# # - run: curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/main/install.sh | sudo bash +# # But, as of 2026-08-31, it requires an --org=... argument, so disable it. +# [[repos]] +# repo = "https://github.com/zahorniak/pre-commit-circleci" +# rev = "v1.3.0" +# hooks = [{ id = "circleci_validate", args = ["--org=gh/mernst"] }] ## GitHub Actions From 1a8ea074943ebf6a73e91b0c1d1941d1dba1221b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 17:11:23 -0700 Subject: [PATCH 269/308] Set container --- .github/workflows/ci.yml | 2 ++ .github/workflows/ci.yml.m4 | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d2408deadda..19337b7b98c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -352,6 +352,8 @@ jobs: - guava_part2_jdk25 - plume_lib_jdk25 runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest steps: - name: Fail if any dependency failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') diff --git a/.github/workflows/ci.yml.m4 b/.github/workflows/ci.yml.m4 index 787d77d388bb..42f238c5171d 100644 --- a/.github/workflows/ci.yml.m4 +++ b/.github/workflows/ci.yml.m4 @@ -75,6 +75,8 @@ include([../../.azure/jobs.m4])dnl - guava_part2_jdk25 - plume_lib_jdk25 runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk25:latest steps: - name: Fail if any dependency failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') From 16c1d975cacd6d1654c0cdac616081b393bdaf5f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 20:07:29 -0700 Subject: [PATCH 270/308] Use correct container --- .github/workflows/ci.yml | 2 ++ .github/workflows/ci.yml.m4 | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19337b7b98c2..b2f7c4342952 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: run: true ci_info: runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk21-plus:latest steps: - uses: actions/checkout@v7 with: diff --git a/.github/workflows/ci.yml.m4 b/.github/workflows/ci.yml.m4 index 42f238c5171d..684f9884fd2e 100644 --- a/.github/workflows/ci.yml.m4 +++ b/.github/workflows/ci.yml.m4 @@ -49,6 +49,8 @@ jobs: run: true ci_info: runs-on: ubuntu-latest + container: + image: mdernst/cf-ubuntu-jdk21-plus:latest steps: - uses: actions/checkout@v7 with: From f7e343ae283c8db075c3de570b33d9a5f1c03c6c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 20:13:23 -0700 Subject: [PATCH 271/308] Fix container --- .github/workflows/ci.yml | 2 -- .github/workflows/ci.yml.m4 | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2f7c4342952..550c1f81c222 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -354,8 +354,6 @@ jobs: - guava_part2_jdk25 - plume_lib_jdk25 runs-on: ubuntu-latest - container: - image: mdernst/cf-ubuntu-jdk25:latest steps: - name: Fail if any dependency failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') diff --git a/.github/workflows/ci.yml.m4 b/.github/workflows/ci.yml.m4 index 684f9884fd2e..c749c4a51e12 100644 --- a/.github/workflows/ci.yml.m4 +++ b/.github/workflows/ci.yml.m4 @@ -77,8 +77,6 @@ include([../../.azure/jobs.m4])dnl - guava_part2_jdk25 - plume_lib_jdk25 runs-on: ubuntu-latest - container: - image: mdernst/cf-ubuntu-jdk25:latest steps: - name: Fail if any dependency failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') From 40a3c6ff46886385af42bc35d786facd43783218 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 21:07:50 -0700 Subject: [PATCH 272/308] AFU Javadoc --- .../annotator/find/ConstructorInsertion.java | 7 +++++++ .../afu/annotator/find/TypedInsertion.java | 15 ++++++++++++++ .../afu/scenelib/el/AClass.java | 20 +++++++++++++++++++ .../afu/scenelib/el/AElement.java | 15 ++++++++++++++ .../afu/scenelib/el/AField.java | 7 +++++++ .../afu/scenelib/el/ATypeElement.java | 7 +++++++ .../afu/scenelib/util/coll/VivifyingMap.java | 2 ++ .../afu/scenelib/util/coll/WrapperMap.java | 6 +++++- 8 files changed, 78 insertions(+), 1 deletion(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java index 5c92f4698944..ebbab1a8e993 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/ConstructorInsertion.java @@ -5,8 +5,15 @@ import java.util.Set; import org.checkerframework.afu.scenelib.type.Type; +/** + * An insertion for a default constructor, which is generated when a class has no explicit + * constructor but a receiver or a declaration annotation must be written on one. + */ public class ConstructorInsertion extends TypedInsertion { + /** The insertion for the constructor's receiver parameter, or null if there is none. */ private ReceiverInsertion receiverInsertion = null; + + /** The declaration annotations to write on the generated constructor. */ private Set declarationInsertions = new LinkedHashSet<>(); /** diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java index 3f1ec9617a48..f07c71601248 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TypedInsertion.java @@ -30,10 +30,25 @@ public abstract class TypedInsertion extends Insertion { /** The inner types to go on this insertion. See {@link ReceiverInsertion} for more details. */ protected List innerTypeInsertions; + /** + * Creates a new {@code TypedInsertion} whose text is not on its own line. + * + * @param type the type to insert + * @param criteria where to insert the text + * @param innerTypeInsertions the inner types to go on this insertion + */ public TypedInsertion(Type type, Criteria criteria, List innerTypeInsertions) { this(type, criteria, false, innerTypeInsertions); } + /** + * Creates a new {@code TypedInsertion}. + * + * @param type the type to insert + * @param criteria where to insert the text + * @param b if true, insert the text on its own line + * @param innerTypeInsertions the inner types to go on this insertion + */ public TypedInsertion( Type type, Criteria criteria, boolean b, List innerTypeInsertions) { super(criteria, b); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java index 5cd9c9a39162..82cecaed229f 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java @@ -33,13 +33,16 @@ public class AClass extends ADeclaration { */ public final VivifyingMap methods = createMethodMap(); + /** The class's annotated static initializer blocks; map key is the index of the block. */ public final VivifyingMap staticInits = createInitBlockMap(); + /** The class's annotated instance initializer blocks; map key is the index of the block. */ public final VivifyingMap instanceInits = createInitBlockMap(); /** The class's annotated fields; map key is field name. */ public final VivifyingMap fields = AField.newVivifyingLHMap_AF(); + /** The class's annotated field initializers; map key is field name. */ public final VivifyingMap fieldInits = createFieldInitMap(); /** @@ -225,6 +228,11 @@ public R accept(ElementVisitor v, T t) { // Static methods + /** + * Returns a new map from method signature to {@link AMethod}, which vivifies missing values. + * + * @return a new vivifying map from method signature to {@link AMethod} + */ private static VivifyingMap createMethodMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override @@ -239,6 +247,12 @@ public boolean isEmptyValue(AMethod v) { }; } + /** + * Returns a new map from initializer block index to {@link ABlock}, which vivifies missing + * values. + * + * @return a new vivifying map from initializer block index to {@link ABlock} + */ private static VivifyingMap createInitBlockMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override @@ -253,6 +267,12 @@ public boolean isEmptyValue(ABlock v) { }; } + /** + * Returns a new map from field name to the {@link AExpression} for its initializer, which + * vivifies missing values. + * + * @return a new vivifying map from field name to field initializer + */ private static VivifyingMap createFieldInitMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java index 836de23dc813..27d02231895a 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java @@ -194,6 +194,13 @@ public R accept(ElementVisitor v, T t) { // Static methods + /** + * Returns a new {@link LinkedHashMap}-backed map from keys to {@link AElement}s, which vivifies + * missing values. + * + * @param the type of the map keys + * @return a new vivifying map from keys to {@link AElement}s + */ static VivifyingMap newVivifyingLHMap_AE() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override @@ -224,6 +231,14 @@ public boolean isEmptyValue(AElement v) { }; } + /** + * Copies the contents of {@code orig} into {@code copy}, cloning each value. + * + * @param the type of the map keys + * @param the type of the map values + * @param orig the map to copy from + * @param copy the map to copy into + */ @SuppressWarnings("unchecked") static void copyMapContents( VivifyingMap orig, VivifyingMap copy) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java index 099f2406e933..7ee0bd2f1bc1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java @@ -158,6 +158,13 @@ public R accept(ElementVisitor v, T t) { return v.visitField(this, t); } + /** + * Returns a new {@link LinkedHashMap}-backed map from keys to {@link AField}s, which vivifies + * missing values. + * + * @param the type of the map keys + * @return a new vivifying map from keys to {@link AField}s + */ static VivifyingMap newVivifyingLHMap_AF() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java index f3edd6e56a66..5590e7164099 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java @@ -111,6 +111,13 @@ public R accept(ElementVisitor v, T t) { return v.visitTypeElement(this, t); } + /** + * Returns a new {@link LinkedHashMap}-backed map from keys to {@link ATypeElement}s, which + * vivifies missing values. + * + * @param the type of the map keys + * @return a new vivifying map from keys to {@link ATypeElement}s + */ static VivifyingMap newVivifyingLHMap_ATE() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index ea8e3f0d2b37..092cc8bb1c7a 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -22,6 +22,8 @@ public abstract class VivifyingMap extends WrapperMap { * generally provides a superset of the functionality of the backing map, it is rarely useful to * access the backing map directly; the parameter is given mainly so you can provide a new map of * your favorite class ({@link java.util.HashMap}, {@link java.util.LinkedHashMap}, etc.). + * + * @param back the backing map */ public VivifyingMap(Map back) { super(back); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 77ec4660a43d..e4f77cba3643 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -17,7 +17,11 @@ public class WrapperMap implements Map { /** The backing map. */ protected final Map back; - /** Constructs a new {@link WrapperMap} with the given backing map. */ + /** + * Constructs a new {@link WrapperMap} with the given backing map. + * + * @param back the backing map + */ protected WrapperMap(Map back) { this.back = back; } From b6d904008dec603e8aaf3d96cfb2ad827d4e255d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 31 Aug 2026 21:11:26 -0700 Subject: [PATCH 273/308] Add signature annotation --- .../checker/modifiability/ModifiabilityBaseVisitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 8259bdbb19e3..646c537887cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -191,7 +191,7 @@ private void checkImplOK( MethodTree method, AnnotationMirror receiverAnno, AnnotationMirror constructorAnno, - String className) { + @FullyQualifiedName String className) { if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation)) { return; } From 317692a1ded118dc888e69644a7a5e10900e607c Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Tue, 1 Sep 2026 07:26:36 -0700 Subject: [PATCH 274/308] Move UnmodifiableParam code --- .../ModifiabilityBaseVisitor.java | 102 +----------------- .../modifiability/ModifiabilityChecker.java | 6 ++ .../modifiability/ModifiabilityVisitor.java | 67 ++++++++++++ .../iterator/IteratorVisitor.java | 6 -- .../modifiability/replace/ReplaceVisitor.java | 6 -- .../modifiability/seqgrow/SeqGrowVisitor.java | 6 -- .../modifiability/shrink/ShrinkVisitor.java | 6 -- .../framework/source/SourceVisitor.java | 2 +- 8 files changed, 76 insertions(+), 125 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 646c537887cf..ca9eeba6534b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.modifiability; -import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; @@ -12,11 +11,7 @@ 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 com.sun.source.util.TreeScanner; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -24,7 +19,6 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; -import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -47,9 +41,6 @@ public class ModifiabilityBaseVisitor extends BaseTypeVisitor { - /** Method-scoped {@code @UnmodifiableParam} annotations allowed by their parameter location. */ - private final Deque> allowedUnmodifiableParamAnnotations = new ArrayDeque<>(); - // /** Package that contains the modifiability type-use qualifiers. */ // private static final String MODIFIABILITY_QUAL_PACKAGE = // "org.checkerframework.checker.modifiability.qual"; @@ -72,38 +63,6 @@ protected void checkThisOrSuperConstructorCall( // Nothing to do; it is handled by `processClassTree()`. } - /** - * Collects the {@code @UnmodifiableParam} annotations that are permitted in this method's formal - * and receiver parameters before visiting the method body. {@link #visitAnnotation} uses the - * stack entry to distinguish allowed parameter annotations from disallowed uses elsewhere in the - * same method. - */ - @Override - public void processMethodTree(String className, MethodTree tree) { - Set allowedAnnotations = new HashSet<>(); - for (VariableTree parameter : tree.getParameters()) { - allowedAnnotations.addAll(unmodifiableParamAnnotations(parameter)); - } - VariableTree receiverParameter = tree.getReceiverParameter(); - if (receiverParameter != null) { - allowedAnnotations.addAll(unmodifiableParamAnnotations(receiverParameter)); - } - allowedUnmodifiableParamAnnotations.push(allowedAnnotations); - super.processMethodTree(className, tree); - allowedUnmodifiableParamAnnotations.pop(); - } - - @Override - public Void visitAnnotation(AnnotationTree tree, Void p) { - if (shouldCheckModifiabilityAnnotationValidity() && isUnmodifiableParamAnnotation(tree)) { - if (allowedUnmodifiableParamAnnotations.isEmpty() - || !allowedUnmodifiableParamAnnotations.getFirst().contains(tree)) { - checker.reportError(tree, "unmodparam.location"); - } - } - return super.visitAnnotation(tree, p); - } - @Override public void processClassTree(ClassTree tree) { super.processClassTree(tree); @@ -250,6 +209,8 @@ private boolean implIsUOE(MethodTree method) { ExpressionTree identifier = nct.getIdentifier(); if (identifier instanceof IdentifierTree it) { // TODO: This can be fooled if a different UnsupportedOperationException is imported. + // You can check the type of exception: + // types.isSameType(TreeUtils.typeOf(exception), ...); return it.getName().contentEquals("UnsupportedOperationException"); } else if (identifier instanceof MemberSelectTree mst) { // TODO: For efficiency, to avoid call to `toString()`, could walk down the MemberSelectTree. @@ -350,37 +311,6 @@ protected boolean shouldCheckReceiverOverrideCapabilityPreservation() { return true; } - /** - * Returns true if this checker should report diagnostics about modifiability annotations whose - * validity is independent of a particular capability hierarchy. - * - *

The default is {@code true}. SeqGrow, Shrink, Replace, and Iterator override this to avoid - * repeating diagnostics when running under the aggregate {@link ModifiabilityChecker}. - * - * @return true if this visitor should report shared modifiability annotation diagnostics - */ - protected boolean shouldCheckModifiabilityAnnotationValidity() { - return true; - } - - /** - * Returns true if {@code tree} is an {@code @}{@link UnmodifiableParam} annotation. - * - * @param tree an annotation tree - * @return true if {@code tree} is an {@code @UnmodifiableParam} annotation - */ - private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { - String annotationName = tree.getAnnotationType().toString(); - // Quick check to avoid expensive annotation resolution for most annotations. - if (!annotationName.equals("UnmodifiableParam") - && !annotationName.endsWith(".UnmodifiableParam")) { - return false; - } - - AnnotationMirror annotation = TreeUtils.annotationFromAnnotationTree(tree); - return annotation != null && atypeFactory.areSameByClass(annotation, UnmodifiableParam.class); - } - // /** // * Returns true if {@code annotation} is a non-maybe modifiability qualifier that (if written // on a @@ -409,34 +339,6 @@ private boolean isUnmodifiableParamAnnotation(AnnotationTree tree) { // return !isTopQualifier; // } - /** - * Returns all {@code @UnmodifiableParam} annotations written on a formal or receiver parameter's - * type. An annotation before the parameter type may appear in the parameter's modifiers, while an - * annotation inside a generic or array type appears in the parameter's type tree. - * - * @param parameter a formal or receiver parameter - * @return the {@code @UnmodifiableParam} annotation trees that are allowed by this parameter - * location - */ - private Set unmodifiableParamAnnotations(VariableTree parameter) { - Set annotations = new HashSet<>(); - TreeScanner scanner = - new TreeScanner<>() { - @Override - public Void visitAnnotation(AnnotationTree tree, Void p) { - if (isUnmodifiableParamAnnotation(tree)) { - annotations.add(tree); - } - return super.visitAnnotation(tree, p); - } - }; - - // Scan both javac locations for parameter type annotations. - // search for method(@UnmodifiableParam List<> param) - scanner.scan(parameter, null); - return annotations; - } - // Suppresses the framework's "constructor result must be TOP" check. // Collection constructors (e.g., new ArrayList()) legitimately produce @Modifiable, which is a // subtype of the top type @MaybeModifiable. This suppression is sound: constructors are diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index c105c00aec97..14a9942a3edf 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -9,6 +9,7 @@ import org.checkerframework.checker.modifiability.shrink.ShrinkChecker; import org.checkerframework.framework.source.AggregateChecker; import org.checkerframework.framework.source.SourceChecker; +import org.checkerframework.framework.source.SourceVisitor; /** * A type-checker that warns, at compile time, if a program might throw {@link @@ -38,4 +39,9 @@ protected Collection> getSupportedCheckers() { return List.of( GrowChecker.class, SeqGrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); } + + @Override + protected SourceVisitor createSourceVisitor() { + return new ModifiabilityVisitor(this); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java new file mode 100644 index 000000000000..87e7c5348ff4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -0,0 +1,67 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.MethodTree; +import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; +import org.checkerframework.framework.source.SourceVisitor; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; + +/** Visitor for the aggregate ModifiabilityChecker. */ +public class ModifiabilityVisitor extends SourceVisitor { + + /** Fully-qualified name for {@link UnmodifiableParam}. */ + private static final String unmodifiableParamQualifiedName = UnmodifiableParam.class.getName(); + + /** {@link ModifiabilityChecker}. */ + private final ModifiabilityChecker checker; + + /** + * Creates a {@link SourceVisitor} to use for scanning a source tree. + * + * @param checker the modifiability checker to invoke on the input source tree + */ + protected ModifiabilityVisitor(ModifiabilityChecker checker) { + super(checker); + this.checker = checker; + } + + /** True when scanning a formal parameter, including the receiver parameter. */ + boolean inParameterType = false; + + /** + * Collects the {@code @UnmodifiableParam} annotations that are permitted in this method's formal + * and receiver parameters. Pushes them on the stack before visiting the method body, then pops + * the stack. {@link #visitAnnotation} uses the stack entry to distinguish allowed parameter + * annotations from disallowed uses elsewhere in the same method. + */ + @Override + public Void visitMethod(MethodTree tree, Void p) { + storeSuppressWarningsAnno(tree); + scan(tree.getModifiers(), p); + scan(tree.getReturnType(), p); + scan(tree.getTypeParameters(), p); + inParameterType = true; + scan(tree.getParameters(), p); + scan(tree.getReceiverParameter(), p); + inParameterType = false; + scan(tree.getThrows(), p); + scan(tree.getBody(), p); + scan(tree.getDefaultValue(), p); + return null; + } + + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + // The implementation of annotationFromAnnotationTree just returns a field of tree, so it's fine + // to always get it. + AnnotationMirror annotation = TreeUtils.annotationFromAnnotationTree(tree); + if (!inParameterType + && annotation != null + && AnnotationUtils.areSameByName(annotation, unmodifiableParamQualifiedName)) { + checker.reportError(tree, "unmodparam.location"); + } + return super.visitAnnotation(tree, p); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java index 12b8ff131d8c..f68a6141afbc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java @@ -22,10 +22,4 @@ public IteratorVisitor(BaseTypeChecker checker) { protected boolean shouldCheckReceiverOverrideCapabilityPreservation() { return false; } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java index aa4c78ed2e75..edc39049f33d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java @@ -14,10 +14,4 @@ public class ReplaceVisitor extends ModifiabilityBaseVisitor { public ReplaceVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java index 91c21b65cb01..3556138a6996 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java @@ -14,10 +14,4 @@ public class SeqGrowVisitor extends ModifiabilityBaseVisitor { public SeqGrowVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java index e7c4f0eabf6b..e828cd5d29c2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java @@ -14,10 +14,4 @@ public class ShrinkVisitor extends ModifiabilityBaseVisitor { public ShrinkVisitor(BaseTypeChecker checker) { super(checker); } - - @Override - protected boolean shouldCheckModifiabilityAnnotationValidity() { - // When running under ModifiabilityChecker, GrowChecker handles shared annotation diagnostics. - return checker.getParentChecker() == null; - } } diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceVisitor.java b/framework/src/main/java/org/checkerframework/framework/source/SourceVisitor.java index f842a56c0dd5..114d6f1ff414 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceVisitor.java @@ -117,7 +117,7 @@ public R visitMethod(MethodTree tree, P p) { * * @param tree a declaration on which a {@code @SuppressWarnings} annotation may be placed */ - private void storeSuppressWarningsAnno(Tree tree) { + protected void storeSuppressWarningsAnno(Tree tree) { if (!warnUnneededSuppressions) { return; } From 9d018773d235d8f5b69d2b0409ac676bc929201c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 1 Sep 2026 08:21:56 -0700 Subject: [PATCH 275/308] Code review --- .../afu/scenelib/el/AElement.java | 5 +- .../afu/scenelib/util/coll/WrapperMap.java | 1 + buildSrc/src/main/groovy/cf-spotless.gradle | 6 + .../modifiability/qual/PolyModifiable.java | 5 + ...ModifiabilityBaseAnnotatedTypeFactory.java | 29 +++- .../ModifiabilityBaseVisitor.java | 75 ++++++---- .../grow/GrowAnnotatedTypeFactory.java | 29 ++-- .../IteratorAnnotatedTypeFactory.java | 5 + .../replace/ReplaceAnnotatedTypeFactory.java | 21 +-- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 21 +-- .../modifiability/seqgrow/SeqGrowChecker.java | 2 + .../modifiability/{ => seqgrow}/ical4j.astub | 0 .../modifiability/seqgrow/javaparser.astub | 13 ++ .../shrink/ShrinkAnnotatedTypeFactory.java | 29 ++-- .../CollectionsModifiableTest.java | 6 +- .../CustomModifiabilityAnnotationWarning.java | 3 +- .../tests/modifiability/HierarchyTest.java | 15 +- .../modifiability/PolyModifiableTest.java | 3 +- .../modifiability/SeqGrowableMapTest.java | 9 +- .../tests/modifiability/SeqGrowableTest.java | 9 +- .../modifiability/UoeImplementationTest.java | 129 ++++++++++++++++++ .../ThrowablePrintStackTrace.java | 3 - 22 files changed, 319 insertions(+), 99 deletions(-) rename checker/src/main/java/org/checkerframework/checker/modifiability/{ => seqgrow}/ical4j.astub (100%) create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub create mode 100644 checker/tests/modifiability/UoeImplementationTest.java diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java index 9ea9650e305d..a35f16b5df48 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java @@ -243,7 +243,10 @@ public boolean isEmptyValue(AElement v) { * @param orig the map to copy from * @param copy the map to copy into */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ + "unchecked", + "nullness:argument" // TODO + }) static void copyMapContents( VivifyingMap orig, @Growable @Replaceable VivifyingMap copy) { for (K key : orig.keySet()) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index ef4e21f5281b..942e09992cb5 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -64,6 +64,7 @@ public boolean isEmpty() { return back.isEmpty(); } + @SuppressWarnings("keyfor") // use of delegate object @Override public @PolyModifiable Set keySet(@PolyModifiable WrapperMap this) { return back.keySet(); diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 388c086d1f77..3c72fc935839 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -94,21 +94,27 @@ spotless { formatAnnotations() .addTypeAnnotation("BottomGrowable") .addTypeAnnotation("BottomReplaceable") + .addTypeAnnotation("BottomSeqGrowable") .addTypeAnnotation("BottomShrinkable") .addTypeAnnotation("Growable") .addTypeAnnotation("IteratorPolyMod") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrowable") + .addTypeAnnotation("PolyIteratorPolyMod") .addTypeAnnotation("PolyModifiable") .addTypeAnnotation("PolyReplaceable") + .addTypeAnnotation("PolySeqGrowable") .addTypeAnnotation("PolyShrinkable") .addTypeAnnotation("Replaceable") + .addTypeAnnotation("SeqGrowable") + .addTypeAnnotation("SeqUngrowable") .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") .addTypeAnnotation("MaybeGrowable") .addTypeAnnotation("MaybeIteratorPolyMod") .addTypeAnnotation("MaybeModifiable") .addTypeAnnotation("MaybeReplaceable") + .addTypeAnnotation("MaybeSeqGrowable") .addTypeAnnotation("MaybeShrinkable") .addTypeAnnotation("Unmodifiable") .addTypeAnnotation("UnmodifiableParam") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 9467ac599891..52fb0a1fd93e 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -1,6 +1,9 @@ package org.checkerframework.checker.modifiability.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; /** @@ -26,5 +29,7 @@ * @checker_framework.manual #modifiability-checker Modifiability Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ +@Documented +@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) public @interface PolyModifiable {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 015a65d840c8..564c6fda6d32 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -92,6 +92,11 @@ protected void refinePreservesModifiabilityReturnType( */ protected void refineIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (!isIteratorMethodInvocation(tree)) { + // Some other method that happens to return an Iterator makes no promise about the + // modifiability of its result. + return; + } AnnotatedTypeMirror returnType = methodType.getReturnType(); // Keep explicit ungrowable/growable iterator contracts (for example, CopyOnWriteArrayList, // ArrayList). @@ -122,6 +127,17 @@ protected void refineIteratorReturnType( } } + /** + * Returns true if {@code tree} is an invocation of {@code iterator()} or {@code listIterator()}. + * + * @param tree a method invocation + * @return true if {@code tree} is an invocation of {@code iterator()} or {@code listIterator()} + */ + private static boolean isIteratorMethodInvocation(MethodInvocationTree tree) { + String methodName = TreeUtils.methodName(tree).toString(); + return methodName.equals("iterator") || methodName.equals("listIterator"); + } + /** * Returns the positive capability qualifier. * @@ -130,12 +146,23 @@ protected void refineIteratorReturnType( protected abstract AnnotationMirror positiveCapability(); /** - * Returns the negative capability qualifier. + * Returns the negative capability qualifier. Only call this method if {@link + * #hasNegativeCapability} returns true. * * @return the negative capability qualifier */ protected abstract AnnotationMirror negativeCapability(); + /** + * Returns true if this checker's hierarchy contains a negative qualifier, such as + * {@code @Ungrowable}. The Iterator hierarchy does not. + * + * @return true if this checker's hierarchy contains a negative qualifier + */ + protected boolean hasNegativeCapability() { + return true; + } + /** * Returns the polymorphic capability qualifier. * diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index ca9eeba6534b..3ba2c5edd0b7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -129,6 +129,10 @@ private void processClassConstructors(ClassTree tree) { // different type than this class's constructors. for (MethodTree method : methods) { + if (method.getBody() == null) { + // The method is abstract or native, so it has no implementation to check. + continue; + } AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); if (receiverType != null) { AnnotationMirror receiverAnno = receiverType.getAnnotation(); @@ -143,6 +147,7 @@ private void processClassConstructors(ClassTree tree) { * Issues an error if the method body does not conform to the given annotation. * * @param method a method declaration + * @param receiverAnno the annotation on the method's receiver parameter * @param constructorAnno the annotation on the class constructors * @param className the name of the enclosing class, used only for diagnostic messages */ @@ -151,39 +156,57 @@ private void checkImplOK( AnnotationMirror receiverAnno, AnnotationMirror constructorAnno, @FullyQualifiedName String className) { - if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation)) { + // Every modifiability hierarchy contains a top qualifier, a positive qualifier, and a + // polymorphic qualifier. Every hierarchy but the Iterator one also contains a negative and a + // bottom qualifier. + if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation) + || AnnotationUtils.areSameByName(receiverAnno, atypeFactory.polyCapability()) + || isNegativeCapability(receiverAnno)) { + // Nothing to check. return; } - - String receiverAnnoName = - receiverAnno.getAnnotationType().asElement().getSimpleName().toString(); - - if (receiverAnnoName.startsWith("Bottom")) { + if (!AnnotationUtils.areSameByName(receiverAnno, positiveCapability())) { + // The only qualifier left is the bottom one. checker.reportError(method, "bottom.annotation.on.receiver"); classWarned.add(className); - } else if (receiverAnnoName.startsWith("Un")) { - // Nothing to check. - } else if (receiverAnnoName.startsWith("Poly")) { - // Nothing to check? + return; + } + + // The receiver requires the capability, so whether the method body should throw + // UnsupportedOperationException depends on the constructor annotation. + if (!atypeFactory.hasNegativeCapability()) { + // The Iterator hierarchy has no negative qualifier: @IteratorPolyMod states what a + // collection's iterator preserves, not whether a method throws + // UnsupportedOperationException. + return; + } + String constructorAnnoName = + constructorAnno.getAnnotationType().asElement().getSimpleName().toString(); + if (isNegativeCapability(constructorAnno)) { + if (!implIsUOE(method)) { + checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); + classWarned.add(className); + } } else { - // The receiver annotation is positive (does not start with "Un"). - // Behavior now depends on the constructor annotation. - String constructorAnnoName = - constructorAnno.getAnnotationType().asElement().getSimpleName().toString(); - if (constructorAnnoName.startsWith("Un")) { - if (!implIsUOE(method)) { - checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); - classWarned.add(className); - } - } else { - if (implIsUOE(method)) { - checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); - classWarned.add(className); - } + if (implIsUOE(method)) { + checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); + classWarned.add(className); } } } + /** + * Returns true if {@code anno} is this checker's negative qualifier, such as {@code @Ungrowable}. + * Always returns false for the Iterator Checker, whose hierarchy has no negative qualifier. + * + * @param anno an annotation in this checker's modifiability hierarchy + * @return true if {@code anno} is this checker's negative qualifier + */ + private boolean isNegativeCapability(AnnotationMirror anno) { + return atypeFactory.hasNegativeCapability() + && AnnotationUtils.areSameByName(anno, atypeFactory.negativeCapability()); + } + /** * Returns true if the method body is exactly {@code throws new * UnsupportedOperationException(...)}. @@ -194,6 +217,10 @@ private void checkImplOK( */ private boolean implIsUOE(MethodTree method) { BlockTree body = method.getBody(); + if (body == null) { + // The method is abstract or native, so it has no body. + return false; + } List statements = body.getStatements(); if (statements.size() != 1) { return false; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index c83b7bf1f218..a0e6357ae85c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -114,28 +114,33 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrowable}, but * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its * grow bit is therefore {@code @MaybeGrowable}. + * + *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural + * weakening is applied. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (tm != null) { - if (areSameByClass(annotation, Modifiable.class)) { - return typeCannotGrow(tm) ? MAYBE_GROWABLE : GROWABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return typeCannotGrow(tm) ? MAYBE_GROWABLE : UNGROWABLE; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) - ? MAYBE_GROWABLE - : POLY_GROWABLE; - } - } - if (areSameByClass(annotation, MaybeModifiable.class) + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeCannotGrow(tm) ? MAYBE_GROWABLE : GROWABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeCannotGrow(tm) ? MAYBE_GROWABLE : UNGROWABLE; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return tm != null && TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) + ? MAYBE_GROWABLE + : POLY_GROWABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_GROWABLE; } return super.canonicalAnnotation(annotation); } + @Override + public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { + return canonicalAnnotation(annotation, null); + } + @Override protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index 67d9d4be105c..b782076df02a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -55,6 +55,11 @@ protected AnnotationMirror negativeCapability() { throw new UnsupportedOperationException("The iterator hierarchy has no negative qualifier."); } + @Override + protected boolean hasNegativeCapability() { + return false; + } + @Override protected AnnotationMirror polyCapability() { return POLY_ITERATOR_POLY_MOD; diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 89ba3d65ccf7..179a0fac0e06 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -130,18 +130,18 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @PolyModifiable} remains {@code @PolyReplaceable}: unlike grow and shrink for {@code * Map.Entry}, replacement through {@code Entry.setValue} is a meaningful capability to carry from * the map receiver. + * + *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural + * weakening is applied. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (tm != null) { - if (areSameByClass(annotation, Modifiable.class)) { - return typeCannotReplace(tm) ? MAYBE_REPLACEABLE : REPLACEABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return typeCannotReplace(tm) ? MAYBE_REPLACEABLE : UNREPLACEABLE; - } - } - if (areSameByClass(annotation, MaybeModifiable.class) + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACEABLE : REPLACEABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACEABLE : UNREPLACEABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_REPLACEABLE; } else if (areSameByClass(annotation, PolyModifiable.class)) { @@ -150,6 +150,11 @@ public AnnotationMirror canonicalAnnotation( return super.canonicalAnnotation(annotation); } + @Override + public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { + return canonicalAnnotation(annotation, null); + } + @Override protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index f3ed9e791119..6548d0df31e6 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -114,18 +114,18 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types * that structurally cannot support sequenced grow operations, such as {@code Iterator}, their * SeqGrow component canonicalizes to {@code @MaybeSeqGrowable}. + * + *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural + * weakening is applied. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (tm != null) { - if (areSameByClass(annotation, Modifiable.class)) { - return typeCanSeqGrow(tm) ? SEQ_GROWABLE : MAYBE_SEQ_GROWABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return typeCanSeqGrow(tm) ? SEQ_UNGROWABLE : MAYBE_SEQ_GROWABLE; - } - } - if (areSameByClass(annotation, MaybeModifiable.class) + if (areSameByClass(annotation, Modifiable.class)) { + return tm == null || typeCanSeqGrow(tm) ? SEQ_GROWABLE : MAYBE_SEQ_GROWABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm == null || typeCanSeqGrow(tm) ? SEQ_UNGROWABLE : MAYBE_SEQ_GROWABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_SEQ_GROWABLE; } else if (areSameByClass(annotation, PolyModifiable.class)) { @@ -134,6 +134,11 @@ public AnnotationMirror canonicalAnnotation( return super.canonicalAnnotation(annotation); } + @Override + public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { + return canonicalAnnotation(annotation, null); + } + @Override protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java index 4d9f57ab3093..989ad67703c7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java @@ -5,6 +5,7 @@ import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -19,6 +20,7 @@ * @checker_framework.manual #modifiability-checker Modifiability Checker */ @RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) +@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"seqgrowable", "modifiability"}) public class SeqGrowChecker extends ModifiabilityBaseChecker { /** Creates a SeqGrowChecker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/ical4j.astub similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub rename to checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/ical4j.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub new file mode 100644 index 000000000000..cc63c87a8309 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub @@ -0,0 +1,13 @@ +import org.checkerframework.checker.modifiability.qual.Modifiable; + +package com.github.javaparser.ast.stmt; + +class BlockStmt { + public @Modifiable NodeList getStatements() {} +} + +package com.github.javaparser.ast; + +class CompilationUnit { + public @Modifiable NodeList getImports() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 31325295089c..0cba6e40e4c9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -108,28 +108,33 @@ protected Set> createSupportedTypeQualifiers() { *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrinkable}, but * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its * shrink bit is therefore {@code @MaybeShrinkable}. + * + *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural + * weakening is applied. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (tm != null) { - if (areSameByClass(annotation, Modifiable.class)) { - return typeCannotShrink(tm) ? MAYBE_SHRINKABLE : SHRINKABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return typeCannotShrink(tm) ? MAYBE_SHRINKABLE : UNSHRINKABLE; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) - ? MAYBE_SHRINKABLE - : POLY_SHRINKABLE; - } - } - if (areSameByClass(annotation, MaybeModifiable.class) + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINKABLE : SHRINKABLE; + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINKABLE : UNSHRINKABLE; + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return tm != null && TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) + ? MAYBE_SHRINKABLE + : POLY_SHRINKABLE; + } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return MAYBE_SHRINKABLE; } return super.canonicalAnnotation(annotation); } + @Override + public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { + return canonicalAnnotation(annotation, null); + } + @Override protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index bbfb0402a693..d221179c15b1 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -289,14 +289,12 @@ void testUnmodifiableWrappers() { // :: error: [assignment] @Modifiable Map modM = Collections.unmodifiableMap(m); - @Growable @Shrinkable @Replaceable @SeqUngrowable - SortedSet ss = new TreeSet<>(); + @Growable @Shrinkable @Replaceable @SeqUngrowable SortedSet ss = new TreeSet<>(); @Unmodifiable SortedSet unmodSS = Collections.unmodifiableSortedSet(ss); // :: error: [method.invocation] unmodSS.add("a"); - @Growable @Shrinkable @Replaceable @SeqUngrowable - SortedMap sm = new TreeMap<>(); + @Growable @Shrinkable @Replaceable @SeqUngrowable SortedMap sm = new TreeMap<>(); @Unmodifiable SortedMap unmodSM = Collections.unmodifiableSortedMap(sm); // :: error: [method.invocation] unmodSM.put("a", "b"); diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java index 915f4c747d78..67c893f1b039 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java @@ -124,8 +124,7 @@ public int size() { implements Iterator {} static class ConstructorLevelMaybeSeqGrowableList extends AbstractList { - @MaybeSeqGrowable - ConstructorLevelMaybeSeqGrowableList() {} + @MaybeSeqGrowable ConstructorLevelMaybeSeqGrowableList() {} @Override public String get(int index) { diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java index c0094dae5f41..042940913c44 100644 --- a/checker/tests/modifiability/HierarchyTest.java +++ b/checker/tests/modifiability/HierarchyTest.java @@ -158,16 +158,11 @@ void testSeqGrowCombinations( // :: error: [assignment] @SeqGrowable Object sgv5 = unknown; - @MaybeGrowable @MaybeSeqGrowable - Object tv1 = gsg; - @MaybeGrowable @MaybeSeqGrowable - Object tv2 = g; - @MaybeGrowable @MaybeSeqGrowable - Object tv3 = sg; - @MaybeGrowable @MaybeSeqGrowable - Object tv4 = none; - @MaybeGrowable @MaybeSeqGrowable - Object tv5 = unknown; + @MaybeGrowable @MaybeSeqGrowable Object tv1 = gsg; + @MaybeGrowable @MaybeSeqGrowable Object tv2 = g; + @MaybeGrowable @MaybeSeqGrowable Object tv3 = sg; + @MaybeGrowable @MaybeSeqGrowable Object tv4 = none; + @MaybeGrowable @MaybeSeqGrowable Object tv5 = unknown; } void testCombinations( diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 5e54b20a874d..ba5323d5ff5f 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -20,8 +20,7 @@ public class PolyModifiableTest { /** A simple polymorphic identity method that preserves all four capabilities. */ - @PolyModifiable @PolyIteratorPolyMod - List identity(@PolyModifiable @PolyIteratorPolyMod List x) { + @PolyModifiable @PolyIteratorPolyMod List identity(@PolyModifiable @PolyIteratorPolyMod List x) { return x; } diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java index 8e97b2818cda..3e944e7f688e 100644 --- a/checker/tests/modifiability/SeqGrowableMapTest.java +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -60,8 +60,7 @@ void ordinaryGrowAndReplaceDoNotImplySeqGrow( } void sortedMapsAreSeqUngrowable() { - @Growable @Shrinkable @Replaceable @SeqUngrowable - TreeMap treeMap = new TreeMap<>(); + @Growable @Shrinkable @Replaceable @SeqUngrowable TreeMap treeMap = new TreeMap<>(); treeMap.put("a", 1); treeMap.remove("a"); treeMap.put("b", 2); @@ -69,8 +68,7 @@ void sortedMapsAreSeqUngrowable() { // :: error: [method.invocation] treeMap.putFirst("c", 4); - @Growable @Shrinkable @Replaceable @SeqUngrowable - ConcurrentSkipListMap concurrentSkipListMap = new ConcurrentSkipListMap<>(); + @Growable @Shrinkable @Replaceable @SeqUngrowable ConcurrentSkipListMap concurrentSkipListMap = new ConcurrentSkipListMap<>(); concurrentSkipListMap.put("a", 1); concurrentSkipListMap.remove("a"); concurrentSkipListMap.put("b", 2); @@ -84,8 +82,7 @@ void sortedMapsAreSeqUngrowable() { // :: error: [assignment] @Modifiable TreeMap modifiableTreeMap = new TreeMap<>(); - @SeqGrowable - ConcurrentSkipListMap seqGrowableConcurrentSkipListMap = + @SeqGrowable ConcurrentSkipListMap seqGrowableConcurrentSkipListMap = // :: error: [assignment] new ConcurrentSkipListMap<>(); diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java index a9eccbdf5d91..6a42f067ce77 100644 --- a/checker/tests/modifiability/SeqGrowableTest.java +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -31,8 +31,7 @@ void constructorPositiveCases() { @SeqGrowable Vector vector = new Vector<>(); @SeqGrowable LinkedHashSet linkedHashSet = new LinkedHashSet<>(); @SeqGrowable CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList<>(); - @SeqGrowable - ConcurrentLinkedDeque concurrentLinkedDeque = new ConcurrentLinkedDeque<>(); + @SeqGrowable ConcurrentLinkedDeque concurrentLinkedDeque = new ConcurrentLinkedDeque<>(); @SeqGrowable LinkedBlockingDeque linkedBlockingDeque = new LinkedBlockingDeque<>(); arrayDeque.addFirst("a"); @@ -78,14 +77,12 @@ void sortedSetsAreSeqUngrowable() { growableTreeSet.add("a"); @SeqUngrowable TreeSet seqUngrowableTreeSet = new TreeSet<>(); - @SeqUngrowable - ConcurrentSkipListSet seqUngrowableConcurrentSet = new ConcurrentSkipListSet<>(); + @SeqUngrowable ConcurrentSkipListSet seqUngrowableConcurrentSet = new ConcurrentSkipListSet<>(); // :: error: [assignment] @SeqGrowable TreeSet seqGrowableTreeSet = new TreeSet<>(); - @SeqGrowable - ConcurrentSkipListSet seqGrowableConcurrentSet = + @SeqGrowable ConcurrentSkipListSet seqGrowableConcurrentSet = // :: error: [assignment] new ConcurrentSkipListSet<>(); } diff --git a/checker/tests/modifiability/UoeImplementationTest.java b/checker/tests/modifiability/UoeImplementationTest.java new file mode 100644 index 000000000000..a7757b7935d9 --- /dev/null +++ b/checker/tests/modifiability/UoeImplementationTest.java @@ -0,0 +1,129 @@ +// Tests the `method.implementation.is.uoe` and `method.implementation.not.uoe` checks, which +// compare a method body against the modifiability annotation on the class's constructors. + +import java.util.AbstractList; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; + +public class UoeImplementationTest { + + // All constructors are @Ungrowable, so a method with a @Growable receiver must throw + // UnsupportedOperationException. + static class UngrowableList extends AbstractList { + @Ungrowable UngrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void growThrows(@Growable UngrowableList this) { + throw new UnsupportedOperationException(); + } + + // :: error: (method.implementation.not.uoe) + public void growDoesNotThrow(@Growable UngrowableList this) {} + } + + // All constructors are @Growable, so a method with a @Growable receiver must not throw + // UnsupportedOperationException. + static class GrowableList extends AbstractList { + @Growable GrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void growDoesNotThrow(@Growable GrowableList this) {} + + // :: error: (method.implementation.is.uoe) + public void growThrows(@Growable GrowableList this) { + throw new UnsupportedOperationException(); + } + } + + // A method with no body has no implementation to check. + abstract static class AbstractUngrowableList extends AbstractList { + @Ungrowable AbstractUngrowableList() {} + + public abstract void growAbstract(@Growable AbstractUngrowableList this); + } + + // The SeqGrow hierarchy behaves like the Grow hierarchy, even though its negative qualifier is + // named `@SeqUngrowable` rather than starting with "Un". + static class SeqUngrowableList extends AbstractList { + @SeqUngrowable SeqUngrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void seqGrowThrows(@SeqGrowable SeqUngrowableList this) { + throw new UnsupportedOperationException(); + } + + // :: error: (method.implementation.not.uoe) + public void seqGrowDoesNotThrow(@SeqGrowable SeqUngrowableList this) {} + } + + static class SeqGrowableList extends AbstractList { + @SeqGrowable SeqGrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void seqGrowDoesNotThrow(@SeqGrowable SeqGrowableList this) {} + + // :: error: (method.implementation.is.uoe) + public void seqGrowThrows(@SeqGrowable SeqGrowableList this) { + throw new UnsupportedOperationException(); + } + } + + // The Iterator hierarchy has no negative qualifier: @IteratorPolyMod says what a collection's + // iterator preserves, not whether a method throws UnsupportedOperationException. + static class IteratorPolyModList extends AbstractList { + @IteratorPolyMod IteratorPolyModList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void iteratorPolyModThrows(@IteratorPolyMod IteratorPolyModList this) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java b/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java index 9e61b3aad450..84aaebb00508 100644 --- a/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java +++ b/checker/tests/resourceleak/sideeffect-free-stubs-tests/ThrowablePrintStackTrace.java @@ -12,7 +12,6 @@ class ThrowablePrintStackTrace implements Closeable { private @Owning CloseableResource resource = new CloseableResource(); - @SuppressWarnings("contracts.postcondition") // temporary, to remove before merging @Override @EnsuresCalledMethods(value = "this.resource", methods = "close") public void close() { @@ -26,7 +25,6 @@ class ThrowablePrintStackTracePrintStream implements Closeable { private @Owning CloseableResource resource = new CloseableResource(); - @SuppressWarnings("contracts.postcondition") // temporary, to remove before merging @Override @EnsuresCalledMethods(value = "this.resource", methods = "close") public void close() { @@ -40,7 +38,6 @@ class ThrowablePrintStackTracePrintWriter implements Closeable { private @Owning CloseableResource resource = new CloseableResource(); - @SuppressWarnings("contracts.postcondition") // temporary, to remove before merging @Override @EnsuresCalledMethods(value = "this.resource", methods = "close") public void close() { From 0c9e82d58ec7e4011739bfe4073a92ea66de4e46 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 4 Sep 2026 11:53:35 -0700 Subject: [PATCH 276/308] Code review --- .../afu/scenelib/el/AClass.java | 9 +- .../afu/scenelib/util/coll/VivifyingMap.java | 2 +- .../afu/scenelib/util/coll/WrapperMap.java | 4 +- build.gradle | 3 +- buildSrc/src/main/groovy/cf-spotless.gradle | 12 +- .../modifiability/qual/IteratorPolyMod.java | 3 +- .../qual/MaybeIteratorPolyMod.java | 5 +- .../modifiability/qual/PolyModifiable.java | 11 +- ...ModifiabilityBaseAnnotatedTypeFactory.java | 299 ++++++++++++++---- .../ModifiabilityBaseChecker.java | 18 +- .../ModifiabilityBaseVisitor.java | 40 ++- .../modifiability/ModifiabilityChecker.java | 6 +- .../modifiability/ModifiabilityVisitor.java | 16 +- .../grow/GrowAnnotatedTypeFactory.java | 113 ++----- .../modifiability/grow/GrowVisitor.java | 17 - .../IteratorAnnotatedTypeFactory.java | 35 +- .../iterator/IteratorChecker.java | 6 + .../checker/modifiability/messages.properties | 6 +- .../replace/ReplaceAnnotatedTypeFactory.java | 119 ++----- .../modifiability/replace/ReplaceVisitor.java | 17 - .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 120 ++----- .../modifiability/seqgrow/SeqGrowChecker.java | 6 + .../modifiability/seqgrow/SeqGrowVisitor.java | 17 - .../shrink/ShrinkAnnotatedTypeFactory.java | 102 ++---- .../modifiability/shrink/ShrinkVisitor.java | 17 - .../ConstructorResultTypeTest.java | 63 ++++ .../IdentityHashMapModifiableTest.java | 27 +- .../IteratorConflictingAnnosTypeTuple.java | 5 + .../IteratorConflictingAnnosUse.java | 3 + .../modifiability/IteratorPrecisionTest.java | 12 +- .../modifiability/PolyModifiableTest.java | 8 +- .../tests/modifiability/SeqGrowableTest.java | 2 +- .../modifiability/UoeImplementationTest.java | 8 +- docs/CHANGELOG.md | 4 + docs/manual/advanced-features.tex | 4 +- docs/manual/inference.tex | 4 +- docs/manual/introduction.tex | 3 +- docs/manual/modifiability-checker.tex | 10 +- .../stub/RemoveAnnotationsForInference.java | 1 - 39 files changed, 545 insertions(+), 612 deletions(-) delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java create mode 100644 checker/tests/modifiability/ConstructorResultTypeTest.java diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java index 74ed70563cfe..5a84b0bf1863 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java @@ -237,7 +237,8 @@ public R accept(ElementVisitor v, T t) { * * @return a new vivifying map from method signature to {@link AMethod} */ - @SuppressWarnings("modifiability:return") // TODO + @SuppressWarnings("modifiability:return") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. private static @Modifiable VivifyingMap createMethodMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override @@ -258,7 +259,8 @@ public boolean isEmptyValue(AMethod v) { * * @return a new vivifying map from initializer block index to {@link ABlock} */ - @SuppressWarnings("modifiability:return") // TODO + @SuppressWarnings("modifiability:return") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. private static @Modifiable VivifyingMap createInitBlockMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override @@ -279,7 +281,8 @@ public boolean isEmptyValue(ABlock v) { * * @return a new vivifying map from field name to field initializer */ - @SuppressWarnings("modifiability:return") // TODO + @SuppressWarnings("modifiability:return") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. private static @Modifiable VivifyingMap createFieldInitMap() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index fafef731ccf1..ac6a234c089b 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -53,7 +53,7 @@ public V getVivify(K k) { protected abstract V createValueFor(K k); /** Prunes this map by deleting entries with empty values. */ - @SuppressWarnings("modifiability:assignment") // TODO + @SuppressWarnings("modifiability:assignment") // entrySet() is not @IteratorPolyMod. public void prune() { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 942e09992cb5..387b4c8bbefa 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -4,7 +4,6 @@ import java.util.Map; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; -import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; @@ -100,8 +99,7 @@ public int size() { } @Override - public @IteratorPolyMod @PolyModifiable Collection values( - @PolyModifiable WrapperMap this) { + public @PolyModifiable Collection values(@PolyModifiable WrapperMap this) { return back.values(); } diff --git a/build.gradle b/build.gradle index 0e5a379cf124..1a9402fab8b3 100644 --- a/build.gradle +++ b/build.gradle @@ -975,14 +975,13 @@ subprojects { "org.checkerframework.checker.resourceleak.ResourceLeakChecker") rootProject.createCheckTypeTask(project.name, "Signature", "org.checkerframework.checker.signature.SignatureChecker") - createCheckTypeTask(project.name, "Modifiability", + rootProject.createCheckTypeTask(project.name, "Modifiability", "org.checkerframework.checker.modifiability.ModifiabilityChecker", [ "-Awarns", "-Afilenames", ]) - // The checkNullness task runs on all code, but it only *checks* the following code: // * All files outside the "framework", "checker", and "annotation-file-utilities" subprojects. // * In the "framework", "checker", and "annotation-file-utilities" subprojects, files with `@AnnotatedFor("nullness")`. diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 3c72fc935839..959f8cf7de10 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -98,6 +98,12 @@ spotless { .addTypeAnnotation("BottomShrinkable") .addTypeAnnotation("Growable") .addTypeAnnotation("IteratorPolyMod") + .addTypeAnnotation("MaybeGrowable") + .addTypeAnnotation("MaybeIteratorPolyMod") + .addTypeAnnotation("MaybeModifiable") + .addTypeAnnotation("MaybeReplaceable") + .addTypeAnnotation("MaybeSeqGrowable") + .addTypeAnnotation("MaybeShrinkable") .addTypeAnnotation("Modifiable") .addTypeAnnotation("PolyGrowable") .addTypeAnnotation("PolyIteratorPolyMod") @@ -110,12 +116,6 @@ spotless { .addTypeAnnotation("SeqUngrowable") .addTypeAnnotation("Shrinkable") .addTypeAnnotation("Ungrowable") - .addTypeAnnotation("MaybeGrowable") - .addTypeAnnotation("MaybeIteratorPolyMod") - .addTypeAnnotation("MaybeModifiable") - .addTypeAnnotation("MaybeReplaceable") - .addTypeAnnotation("MaybeSeqGrowable") - .addTypeAnnotation("MaybeShrinkable") .addTypeAnnotation("Unmodifiable") .addTypeAnnotation("UnmodifiableParam") .addTypeAnnotation("Unreplaceable") diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index 0fd6d04fcfd4..65556609bdde 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -19,7 +19,8 @@ * For any collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is * {@code @MaybeShrinkable}. * - *

This annotation is used in GrowChecker, ShrinkChecker, and ReplaceChecker. + *

The Grow, Shrink, and Replace Checkers consult this annotation; the SeqGrow Checker does not, + * because an iterator has no sequenced-grow methods. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java index d7a6ea8b5a49..bee5817cbf58 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java @@ -9,7 +9,10 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * For the annotated collection, the return type of {@code iterator()} is {@code @MaybeShrinkable}. + * The top qualifier in the Iterator hierarchy. The iterator of the annotated collection is not + * known to preserve the collection's modifiability, so the result of {@code iterator()} and {@code + * listIterator()} is the top qualifier in every capability hierarchy: {@code @MaybeShrinkable}, and + * for a {@code ListIterator} also {@code @MaybeGrowable} and {@code @MaybeReplaceable}. * *

This is the default qualifier for unannotated types. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java index 52fb0a1fd93e..f97cd75e264d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -8,8 +8,11 @@ /** * Convenience alias meaning - * {@code @PolyGrowable @PolyReplaceable @PolyShrinkable @PolySeqGrowable @PolyIteratorPolyMod}. A - * polymorphic qualifier for all five modifiability hierarchies. + * {@code @PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable}. A polymorphic qualifier + * for the four capability hierarchies. + * + *

This alias does not expand into the Iterator hierarchy. Write {@code @PolyIteratorPolyMod} + * explicitly to make a method preserve {@code @IteratorPolyMod}. * *

Write {@code @PolyModifiable} on methods that preserve or transfer modifiability, such as * {@code List.subList()}. @@ -17,8 +20,8 @@ *

For example: * *


- * class Example {
- *   @PolyModifiable List<E> subList(@PolyModifiable List<E> this, int from, int to);
+ * interface Example<E> {
+ *   @PolyModifiable List<E> subList(@PolyModifiable Example<E> this, int from, int to);
  * }
  * 
* diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 564c6fda6d32..207ce44e492a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -2,25 +2,48 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; +import java.util.List; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.iterator.IteratorChecker; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** Shared annotated type factory logic for the Modifiability sub-checkers. */ public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + /** The {@code @}{@link IteratorPolyMod} qualifier. */ - protected final AnnotationMirror ITERATOR_PRESERVE_REMOVE; + protected final AnnotationMirror ITERATOR_POLY_MOD; + + /** The erased {@code java.util.Map.Entry} type. */ + protected final TypeMirror mapEntryErasure; + + /** The erased {@code java.util.Iterator} type. */ + protected final TypeMirror iteratorErasure; + + /** The erased {@code java.util.ListIterator} type. */ + protected final TypeMirror listIteratorErasure; - /** The top qualifier in the hierarchy. Subclasses must set this after calling {@code super()}. */ - // Non-final so subclasses can set it. - protected AnnotationMirror topAnnotation; + /** + * The methods whose result this factory refines: {@code Iterable.iterator()}, {@code + * List.listIterator()}, and {@code List.listIterator(int)}. + */ + private final List iteratorMethods; /** * Creates a ModifiabilityBaseAnnotatedTypeFactory. @@ -29,24 +52,192 @@ public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotate */ protected ModifiabilityBaseAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - this.ITERATOR_PRESERVE_REMOVE = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); + this.ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); + this.mapEntryErasure = erasureOf("java.util.Map.Entry"); + this.iteratorErasure = erasureOf("java.util.Iterator"); + this.listIteratorErasure = erasureOf("java.util.ListIterator"); + this.iteratorMethods = + List.of( + TreeUtils.getMethod("java.lang.Iterable", "iterator", 0, processingEnv), + TreeUtils.getMethod("java.util.List", "listIterator", 0, processingEnv), + TreeUtils.getMethod("java.util.List", "listIterator", 1, processingEnv)); } /** - * Refines the return type of a {@code @PreservesModifiability} method. + * Returns the erasure of the named type. + * + * @param canonicalName the canonical name of a type that is always present + * @return the erasure of the named type + */ + protected final TypeMirror erasureOf(String canonicalName) { + return types.erasure(elements.getTypeElement(canonicalName).asType()); + } + + // -- Qualifiers of this checker's hierarchy ---------- + + /** + * Returns the top qualifier of this checker's hierarchy, such as {@code @MaybeGrowable}. + * + * @return the top qualifier of this checker's hierarchy + */ + protected abstract AnnotationMirror topAnnotation(); + + /** + * Returns the positive capability qualifier, such as {@code @Growable}. + * + * @return the positive capability qualifier + */ + protected abstract AnnotationMirror positiveCapability(); + + /** + * Returns the negative capability qualifier, such as {@code @Ungrowable}. Only call this method + * if {@link #hasNegativeCapability} returns true. + * + * @return the negative capability qualifier + */ + protected abstract AnnotationMirror negativeCapability(); + + /** + * Returns the polymorphic capability qualifier, such as {@code @PolyGrowable}. + * + * @return the polymorphic capability qualifier + */ + protected abstract AnnotationMirror polyCapability(); + + /** + * Returns true if this checker's hierarchy contains a negative qualifier, such as + * {@code @Ungrowable}. The Iterator hierarchy does not. + * + * @return true if this checker's hierarchy contains a negative qualifier + */ + protected boolean hasNegativeCapability() { + return true; + } + + // -- Expansion of the whole-modifiability aliases ---------- + + /** + * Returns true if this checker's hierarchy is one of the capabilities that the + * whole-modifiability aliases ({@code @Modifiable}, {@code @Unmodifiable}, + * {@code @MaybeModifiable}, {@code @UnmodifiableParam}, and {@code @PolyModifiable}) expand into. + * The Iterator hierarchy is not: it states what a collection's iterator preserves rather than + * whether a mutating method throws {@link UnsupportedOperationException}. + * + * @return true if the whole-modifiability aliases expand into this checker's hierarchy + */ + protected boolean expandsModifiabilityAliases() { + return true; + } + + /** + * Returns true if {@code type} structurally cannot support this checker's capability, so that + * {@code @Modifiable} and {@code @Unmodifiable} weaken to the top qualifier on {@code type}. For + * example, {@code Map.Entry} cannot grow. + * + * @param type the type on which an alias was written + * @return true if {@code type} structurally cannot support this checker's capability + */ + protected boolean typeLacksCapability(TypeMirror type) { + return false; + } + + /** + * Returns true if {@code @PolyModifiable} weakens to the top qualifier on {@code type}, rather + * than to this checker's polymorphic qualifier. This differs from {@link #typeLacksCapability} + * because a polymorphic qualifier may usefully carry a capability that the type itself cannot + * exercise; for example, {@code Map.Entry} carries the replace capability of its map. + * + * @param type the type on which {@code @PolyModifiable} was written + * @return true if {@code @PolyModifiable} weakens to the top qualifier on {@code type} + */ + protected boolean polyLacksCapability(TypeMirror type) { + return false; + } + + /** + * Expands the whole-modifiability aliases into this hierarchy, with structural weakening only for + * aliases whose meaning depends on the annotated type. * - *

If the method has no parameters or returns {@code void}, then this annotation has no effect. + *

{@code @Modifiable} and {@code @Unmodifiable} claim every component capability, so on a type + * that structurally cannot exercise this checker's capability, they weaken to the top qualifier; + * see {@link #typeLacksCapability}. {@code @PolyModifiable} weakens under the different condition + * of {@link #polyLacksCapability}. * - *

Otherwise, if the first argument is positive (i.e.{@code @Shrinkable}), then the return type - * is also positive. If the first argument is {@code @IteratorPolyMod}, then the return type is - * also {@code @IteratorPolyMod}. + *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural + * weakening is applied. + */ + @Override + public AnnotationMirror canonicalAnnotation( + AnnotationMirror annotation, @Nullable TypeMirror tm) { + if (expandsModifiabilityAliases()) { + if (areSameByClass(annotation, Modifiable.class)) { + return tm != null && typeLacksCapability(tm) ? topAnnotation() : positiveCapability(); + } else if (areSameByClass(annotation, Unmodifiable.class)) { + return tm != null && typeLacksCapability(tm) ? topAnnotation() : negativeCapability(); + } else if (areSameByClass(annotation, PolyModifiable.class)) { + return tm != null && polyLacksCapability(tm) ? topAnnotation() : polyCapability(); + } else if (areSameByClass(annotation, MaybeModifiable.class) + || areSameByClass(annotation, UnmodifiableParam.class)) { + return topAnnotation(); + } + } + return super.canonicalAnnotation(annotation); + } + + @Override + public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { + return canonicalAnnotation(annotation, null); + } + + // -- Refinement of method return types ---------- + + /** + * Returns the erased type that the result of an iterator method must be a subtype of for this + * checker to refine the result, or null if this checker does not refine iterator results. The + * Shrink Checker refines the result of {@code iterator()} and {@code listIterator()}; the Grow + * and Replace Checkers refine only the result of {@code listIterator()}, because a plain {@code + * Iterator} can neither grow nor replace. * - *

For every other case, the return type is top ({@code @Maybe*}). + * @return the erased upper bound of the iterator results this checker refines, or null + */ + protected @Nullable TypeMirror refinedIteratorResultBound() { + return null; + } + + @Override + protected ParameterizedExecutableType methodFromUse( + MethodInvocationTree tree, boolean inferTypeArgs) { + ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); + AnnotatedExecutableType method = mType.executableType(); + + TypeMirror iteratorResultBound = refinedIteratorResultBound(); + if (iteratorResultBound != null + && TypesUtils.isErasedSubtype( + method.getReturnType().getUnderlyingType(), iteratorResultBound, types)) { + refineIteratorReturnType(tree, method); + } + + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refinePreservesModifiabilityReturnType(tree, method); + } + + return mType; + } + + /** + * Refines the return type of a {@code @PreservesModifiability} method. + * + *

If the method has no parameters or returns {@code void}, then the annotation has no effect. * - *

Such method cannot be annotated as {@code @Poly*} because a negative (e.g. - * {@code @Unshrinkable}) input could yield either a shrinkable or unshrinkable result. It would - * be imprecise to always use {@code @Maybe*}, because passing a positive type collection - * guarantees a positive return type. + *

Otherwise, if the first argument has this checker's positive qualifier (for example, + * {@code @Shrinkable}), then so does the return type. For every other first argument, the return + * type is the top qualifier. + * + *

Such a method cannot be annotated as {@code @Poly*}, because a negative (for example, + * {@code @Unshrinkable}) input could yield either a positive or a negative result. It would be + * imprecise to always use the top qualifier, because passing a positive argument guarantees a + * positive return type. * *

This method is called by all five sub-checkers. * @@ -63,35 +254,38 @@ protected void refinePreservesModifiabilityReturnType( AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(positiveCapability())) { returnType.replaceAnnotation(positiveCapability()); - } - if (argumentType.hasPrimaryAnnotation(ITERATOR_PRESERVE_REMOVE)) { - returnType.replaceAnnotation(ITERATOR_PRESERVE_REMOVE); + } else { + returnType.replaceAnnotation(topAnnotation()); } } /** - * Refines {@code iterator()} and {@code listIterator()} return types based on + * Refines the result of {@code iterator()} and {@code listIterator()} based on * {@code @IteratorPolyMod}. * *

{@code iterator()} and {@code listIterator()} cannot be annotated as {@code @PolyModifiable} * because not all collections preserve the modifiability of their iterators. (For example, {@code * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, - * special treatment is needed for Iterator methods. + * special treatment is needed for iterator methods. * - *

If the receiver is {@code @Modifiable} and {@code @IteratorPolyMod}, then the result is - * {@code @Modifiable Iterator}. Otherwise, shrinkability precision is dropped to - * {@code @MaybeModifiable}. + *

An explicitly declared result keeps its declared qualifier. Otherwise, the iterator of a + * receiver with this checker's negative qualifier also has that negative qualifier, and the + * iterator of a receiver that has both this checker's positive qualifier and + * {@code @IteratorPolyMod} has the positive qualifier. In every other case the declared result + * type is left alone, which leaves the top qualifier in place. * - *

This method is called by all three sub-checkers. In ShrinkAnnotatedTypeFactory, this method - * is called by both {@code iterator()} and {@code listIterator()} methods. In - * GrowAnnotatedTypeFactory and ReplaceAnnotatedTypeFactory, this method is only called by {@code - * listIterator()} methods. + *

This method is called by the Grow, Shrink, and Replace Checkers; see {@link + * #refinedIteratorResultBound}. * * @param tree the iterator method invocation * @param methodType the annotated executable type of the invoked method */ protected void refineIteratorReturnType( MethodInvocationTree tree, AnnotatedExecutableType methodType) { + if (!hasNegativeCapability()) { + // The Iterator hierarchy has no negative qualifier, so there is nothing to refine. + return; + } if (!isIteratorMethodInvocation(tree)) { // Some other method that happens to return an Iterator makes no promise about the // modifiability of its result. @@ -112,61 +306,30 @@ protected void refineIteratorReturnType( } AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); - // all ungrowable collections' iterators are ungrowable. + // The iterator of a collection that lacks the capability also lacks the capability. if (receiverType.hasPrimaryAnnotation(negativeCapability())) { returnType.replaceAnnotation(negativeCapability()); return; } - // receiver type is @Modifiable and @IteratorPolyMod + // The receiver has the capability; its iterator does too if the receiver is @IteratorPolyMod. if (receiverType.hasPrimaryAnnotation(positiveCapability())) { - var type = getTypeFactoryOfSubchecker(IteratorChecker.class).getAnnotatedType(receiverTree); - if (type.hasAnnotation(ITERATOR_PRESERVE_REMOVE)) { + AnnotatedTypeMirror iteratorHierarchyType = + getTypeFactoryOfSubchecker(IteratorChecker.class).getAnnotatedType(receiverTree); + if (iteratorHierarchyType.hasPrimaryAnnotation(ITERATOR_POLY_MOD)) { returnType.replaceAnnotation(positiveCapability()); } } } /** - * Returns true if {@code tree} is an invocation of {@code iterator()} or {@code listIterator()}. + * Returns true if {@code tree} is an invocation of {@code Iterable.iterator()}, {@code + * List.listIterator()}, {@code List.listIterator(int)}, or an override of one of those. * * @param tree a method invocation - * @return true if {@code tree} is an invocation of {@code iterator()} or {@code listIterator()} + * @return true if {@code tree} is an invocation of an iterator method */ - private static boolean isIteratorMethodInvocation(MethodInvocationTree tree) { - String methodName = TreeUtils.methodName(tree).toString(); - return methodName.equals("iterator") || methodName.equals("listIterator"); + private boolean isIteratorMethodInvocation(MethodInvocationTree tree) { + return TreeUtils.isMethodInvocation(tree, iteratorMethods, processingEnv); } - - /** - * Returns the positive capability qualifier. - * - * @return the positive capability qualifier - */ - protected abstract AnnotationMirror positiveCapability(); - - /** - * Returns the negative capability qualifier. Only call this method if {@link - * #hasNegativeCapability} returns true. - * - * @return the negative capability qualifier - */ - protected abstract AnnotationMirror negativeCapability(); - - /** - * Returns true if this checker's hierarchy contains a negative qualifier, such as - * {@code @Ungrowable}. The Iterator hierarchy does not. - * - * @return true if this checker's hierarchy contains a negative qualifier - */ - protected boolean hasNegativeCapability() { - return true; - } - - /** - * Returns the polymorphic capability qualifier. - * - * @return the polymorphic capability qualifier - */ - protected abstract AnnotationMirror polyCapability(); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index 480b3dfaee36..150a8c776a02 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -2,7 +2,6 @@ import java.util.Set; import org.checkerframework.checker.modifiability.iterator.IteratorChecker; -import org.checkerframework.checker.modifiability.seqgrow.SeqGrowChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.source.SourceChecker; @@ -21,13 +20,24 @@ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { /** Creates a new ModifiabilityBaseChecker. */ protected ModifiabilityBaseChecker() {} + /** + * Returns true if this checker refines the result of {@code iterator()} and {@code + * listIterator()}, and therefore needs to read the Iterator Checker's qualifiers. The Iterator + * Checker itself does not (it would be its own subchecker), and neither does the SeqGrow Checker, + * because an iterator has no sequenced-grow methods. + * + * @return true if this checker needs the Iterator Checker as a subchecker + */ + protected boolean usesIteratorChecker() { + return true; + } + @Override protected Set> getImmediateSubcheckerClasses() { Set> checkers = super.getImmediateSubcheckerClasses(); - if (this instanceof IteratorChecker || this instanceof SeqGrowChecker) { - return checkers; + if (usesIteratorChecker()) { + checkers.add(IteratorChecker.class); } - checkers.add(IteratorChecker.class); return checkers; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 3ba2c5edd0b7..d5c6480dc2f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -29,13 +29,18 @@ import org.checkerframework.javacutil.TreeUtils; /** - * Base visitor for the modifiability sub-checkers (Grow, SeqGrow, Shrink, Replace). + * Base visitor for the modifiability sub-checkers (Grow, SeqGrow, Shrink, Replace, and Iterator). * - *

This class contains logic shared across all four sub-checkers: + *

This class contains logic shared across all the sub-checkers: * *

    *
  • Suppressing the "constructor result must be TOP" check, since collection constructors may * legitimately produce {@code @Modifiable}. + *
  • Requiring all the constructors of a class to declare the same result qualifier, and + * requiring each method body to agree with that qualifier about whether the method throws + * {@link UnsupportedOperationException}. + *
  • Requiring an override to preserve a positive receiver capability of the method it + * overrides. *
*/ public class ModifiabilityBaseVisitor @@ -104,7 +109,11 @@ private void processClassConstructors(ClassTree tree) { AnnotatedExecutableType constructorType = atypeFactory.getAnnotatedType(constructor); AnnotatedTypeMirror returnType = constructorType.getReturnType(); AnnotationMirror thisResultAnno = - returnType.getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation); + returnType.getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); + if (thisResultAnno == null) { + // The result type has no qualifier in this hierarchy, so there is nothing to compare. + continue; + } if (constructorAnno == null) { constructorAnno = thisResultAnno; } else if (!AnnotationUtils.areSameByName(thisResultAnno, constructorAnno)) { @@ -136,7 +145,9 @@ private void processClassConstructors(ClassTree tree) { AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); if (receiverType != null) { AnnotationMirror receiverAnno = receiverType.getAnnotation(); - checkImplOK(method, receiverAnno, constructorAnno, className); + if (receiverAnno != null) { + checkImplOK(method, receiverAnno, constructorAnno, className); + } } } @@ -159,7 +170,7 @@ private void checkImplOK( // Every modifiability hierarchy contains a top qualifier, a positive qualifier, and a // polymorphic qualifier. Every hierarchy but the Iterator one also contains a negative and a // bottom qualifier. - if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation) + if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation()) || AnnotationUtils.areSameByName(receiverAnno, atypeFactory.polyCapability()) || isNegativeCapability(receiverAnno)) { // Nothing to check. @@ -208,12 +219,11 @@ private boolean isNegativeCapability(AnnotationMirror anno) { } /** - * Returns true if the method body is exactly {@code throws new + * Returns true if the method body is exactly {@code throw new * UnsupportedOperationException(...)}. * * @param method a method declaration - * @return true if the method body is exactly {@code throws new - * UnsupportedOperationException(...)} + * @return true if the method body is exactly {@code throw new UnsupportedOperationException(...)} */ private boolean implIsUOE(MethodTree method) { BlockTree body = method.getBody(); @@ -270,22 +280,20 @@ private AnnotationMirror positiveCapability() { *

For example: * *

{@code
-   * class List {
-   *   void add(@Growable List list) {
-   *     // ...
-   *   }
+   * abstract class Super {
+   *   abstract void add(@Growable Super this, String s);
    * }
    *
-   * class A extends List {
+   * class Sub extends Super {
    *   @Override
-   *   void add() {
+   *   void add(@MaybeGrowable Sub this, String s) {
    *     throw new UnsupportedOperationException();
    *   }
    * }
    * }
* - *

Without requiring the override to preserve the {@code @Growable} receiver, {@code A.add()} - * would be permitted even when {@code A} is only {@code @MaybeMod}. + *

Without requiring the override to preserve the {@code @Growable} receiver, {@code Sub.add()} + * would be permitted even though the body always throws {@link UnsupportedOperationException}. */ @Override protected boolean checkOverride( diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java index 14a9942a3edf..d5862182940c 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -37,7 +37,11 @@ public ModifiabilityChecker() {} @Override protected Collection> getSupportedCheckers() { return List.of( - GrowChecker.class, SeqGrowChecker.class, ShrinkChecker.class, ReplaceChecker.class); + GrowChecker.class, + SeqGrowChecker.class, + ShrinkChecker.class, + ReplaceChecker.class, + IteratorChecker.class); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 87e7c5348ff4..c48494eea3a2 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -17,6 +17,9 @@ public class ModifiabilityVisitor extends SourceVisitor { /** {@link ModifiabilityChecker}. */ private final ModifiabilityChecker checker; + /** True when scanning a formal parameter type, including the receiver parameter. */ + private boolean inParameterType = false; + /** * Creates a {@link SourceVisitor} to use for scanning a source tree. * @@ -27,14 +30,13 @@ protected ModifiabilityVisitor(ModifiabilityChecker checker) { this.checker = checker; } - /** True when scanning a formal parameter, including the receiver parameter. */ - boolean inParameterType = false; - /** - * Collects the {@code @UnmodifiableParam} annotations that are permitted in this method's formal - * and receiver parameters. Pushes them on the stack before visiting the method body, then pops - * the stack. {@link #visitAnnotation} uses the stack entry to distinguish allowed parameter - * annotations from disallowed uses elsewhere in the same method. + * Sets {@link #inParameterType} while scanning this method's formal and receiver parameters, so + * that {@link #visitAnnotation} can distinguish an allowed {@code @UnmodifiableParam} in a + * parameter type from a disallowed one elsewhere in the same method. + * + *

The field needs no save-and-restore discipline, because a method declaration cannot appear + * within a formal parameter. */ @Override public Void visitMethod(MethodTree tree, Void p) { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index a0e6357ae85c..f5ddf28ab69a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -1,47 +1,26 @@ package org.checkerframework.checker.modifiability.grow; -import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.MaybeGrowable; -import org.checkerframework.checker.modifiability.qual.MaybeModifiable; -import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyGrowable; -import org.checkerframework.checker.modifiability.qual.PolyModifiable; -import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Ungrowable; -import org.checkerframework.checker.modifiability.qual.Unmodifiable; -import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link GrowChecker}. */ public class GrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { - /** The erased {@code java.util.Map.Entry} type. */ - private final TypeMirror mapEntryErasure; - - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - - /** The erased {@code java.util.ListIterator} type. */ - private final TypeMirror listIteratorErasure; - - // -- Hierarchy qualifiers ---------- - /** The {@code @}{@link MaybeGrowable} qualifier. */ private final AnnotationMirror MAYBE_GROWABLE; @@ -62,21 +41,18 @@ public class GrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFact @SuppressWarnings("this-escape") public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - - this.mapEntryErasure = types.erasure(elements.getTypeElement("java.util.Map.Entry").asType()); - this.iteratorErasure = types.erasure(elements.getTypeElement("java.util.Iterator").asType()); - this.listIteratorErasure = - types.erasure(elements.getTypeElement("java.util.ListIterator").asType()); - // Initialize annotation mirrors after the hierarchy is established. this.MAYBE_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeGrowable.class); this.GROWABLE = AnnotationBuilder.fromClass(elements, Growable.class); this.UNGROWABLE = AnnotationBuilder.fromClass(elements, Ungrowable.class); this.POLY_GROWABLE = AnnotationBuilder.fromClass(elements, PolyGrowable.class); - this.topAnnotation = MAYBE_GROWABLE; - postInit(); } + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_GROWABLE; + } + @Override protected AnnotationMirror positiveCapability() { return GROWABLE; @@ -104,76 +80,31 @@ protected Set> createSupportedTypeQualifiers() { } /** - * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for - * aliases whose meaning depends on the annotated type. - * - *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types - * that cannot grow structurally, such as {@code Map.Entry} and non-{@code ListIterator} {@code - * Iterator}, their grow component canonicalizes to {@code @MaybeGrowable}. - * - *

{@code @PolyModifiable} is different: it should usually become {@code @PolyGrowable}, but - * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its - * grow bit is therefore {@code @MaybeGrowable}. - * - *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural - * weakening is applied. + * {@code Map.Entry} and a non-{@code ListIterator} {@code Iterator} have no grow methods, so + * {@code @Modifiable} and {@code @Unmodifiable} make no claim about them. */ @Override - public AnnotationMirror canonicalAnnotation( - AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotGrow(tm) ? MAYBE_GROWABLE : GROWABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotGrow(tm) ? MAYBE_GROWABLE : UNGROWABLE; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) - ? MAYBE_GROWABLE - : POLY_GROWABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_GROWABLE; + protected boolean typeLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; } - return super.canonicalAnnotation(annotation); + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types) + || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) + && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } + /** + * For {@code Map.Entry}, only the replace bit is meaningful to carry from the map receiver, so + * the grow bit of {@code @PolyModifiable} is {@code @MaybeGrowable}. + */ @Override - public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { - return canonicalAnnotation(annotation, null); + protected boolean polyLacksCapability(TypeMirror type) { + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } @Override - protected ParameterizedExecutableType methodFromUse( - MethodInvocationTree tree, boolean inferTypeArgs) { - ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); - AnnotatedExecutableType method = mType.executableType(); - - // if the method is listIterator(). - TypeMirror returnUnderlying = method.getReturnType().getUnderlyingType(); - if (TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types)) { - refineIteratorReturnType(tree, method); - } - - // if the method is annotated @PreservesModifiability - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { - refinePreservesModifiabilityReturnType(tree, method); - } - - return mType; - } - - /** - * Returns true if {@code type} structurally cannot support grow operations. - * - * @param type the type to test - * @return true if {@code type} structurally cannot support grow operations - */ - private boolean typeCannotGrow(TypeMirror type) { - if (type.getKind() != TypeKind.DECLARED) { - return false; - } - return TypesUtils.isErasedSubtype(type, mapEntryErasure, types) - || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) - && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); + protected @Nullable TypeMirror refinedIteratorResultBound() { + // A plain Iterator cannot grow, so only a ListIterator result is refined. + return listIteratorErasure; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java deleted file mode 100644 index 411b9f24ab96..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowVisitor.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.checkerframework.checker.modifiability.grow; - -import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; -import org.checkerframework.common.basetype.BaseTypeChecker; - -/** Visitor for the {@link GrowChecker}. */ -public class GrowVisitor extends ModifiabilityBaseVisitor { - - /** - * Creates a visitor for the Grow Checker. - * - * @param checker the Grow Checker - */ - public GrowVisitor(BaseTypeChecker checker) { - super(checker); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index b782076df02a..080d2014306a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -1,21 +1,16 @@ package org.checkerframework.checker.modifiability.iterator; -import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; -import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.TreeUtils; /** The annotated type factory for the {@link IteratorChecker}. */ public class IteratorAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { @@ -23,9 +18,6 @@ public class IteratorAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedType /** The {@code @}{@link MaybeIteratorPolyMod} qualifier. */ private final AnnotationMirror MAYBE_ITERATOR_POLY_MOD; - /** The {@code @}{@link IteratorPolyMod} qualifier. */ - private final AnnotationMirror ITERATOR_POLY_MOD; - /** The {@code @}{@link PolyIteratorPolyMod} qualifier. */ private final AnnotationMirror POLY_ITERATOR_POLY_MOD; @@ -39,12 +31,15 @@ public IteratorAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.MAYBE_ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, MaybeIteratorPolyMod.class); - this.ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); this.POLY_ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, PolyIteratorPolyMod.class); - this.topAnnotation = MAYBE_ITERATOR_POLY_MOD; postInit(); } + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_ITERATOR_POLY_MOD; + } + @Override protected AnnotationMirror positiveCapability() { return ITERATOR_POLY_MOD; @@ -60,6 +55,11 @@ protected boolean hasNegativeCapability() { return false; } + @Override + protected boolean expandsModifiabilityAliases() { + return false; + } + @Override protected AnnotationMirror polyCapability() { return POLY_ITERATOR_POLY_MOD; @@ -71,19 +71,4 @@ protected Set> createSupportedTypeQualifiers() { Arrays.asList( MaybeIteratorPolyMod.class, IteratorPolyMod.class, PolyIteratorPolyMod.class)); } - - @Override - protected ParameterizedExecutableType methodFromUse( - MethodInvocationTree tree, boolean inferTypeArgs) { - ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); - AnnotatedExecutableType method = mType.executableType(); - - // if the method is annotated @PreservesModifiability - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { - refinePreservesModifiabilityReturnType(tree, method); - } - - return mType; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java index 2674577e8b40..3e62d399f699 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java @@ -23,4 +23,10 @@ public class IteratorChecker extends ModifiabilityBaseChecker { /** Creates an IteratorChecker. */ public IteratorChecker() {} + + @Override + protected boolean usesIteratorChecker() { + // This checker would otherwise be its own subchecker. + return false; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties index 04ffd43db6fd..3b4e585ad56a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -1,5 +1,5 @@ -unmodparam.location=@UnmodifiableParam may only be written on a formal parameter or receiver parameter +unmodparam.location=@UnmodifiableParam may only be written within the type of a method or constructor formal parameter, including the receiver parameter inconsistent.constructor.result.type=Constructor result type %s is inconsistent with a previous constructor with result type %s bottom.annotation.on.receiver=Do not write the bottom annotation on a receiver -method.implementation.is.uoe=Method implementation should not be `throws new UnsupportedOperationException()` -method.implementation.not.uoe=Method implementation should be `throws new UnsupportedOperationException()` +method.implementation.is.uoe=Method implementation should not be `throw new UnsupportedOperationException()`, because the constructors of this class are %s +method.implementation.not.uoe=Method implementation should be `throw new UnsupportedOperationException()`, because the constructors of this class are %s diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 179a0fac0e06..19dccf4df0f1 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -1,31 +1,21 @@ package org.checkerframework.checker.modifiability.replace; -import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomReplaceable; -import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; -import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyReplaceable; -import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Replaceable; -import org.checkerframework.checker.modifiability.qual.Unmodifiable; -import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.modifiability.qual.Unreplaceable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ReplaceChecker}. */ @@ -43,14 +33,6 @@ public class ReplaceAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeF /** The erased {@code java.util.LinkedList} type. */ private final TypeMirror linkedListErasure; - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - - /** The erased {@code java.util.ListIterator} type. */ - private final TypeMirror listIteratorErasure; - - // -- Hierarchy qualifiers ---------- - /** The {@code @}{@link MaybeReplaceable} qualifier. */ private final AnnotationMirror MAYBE_REPLACEABLE; @@ -71,27 +53,22 @@ public class ReplaceAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeF @SuppressWarnings("this-escape") public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - - this.setErasure = types.erasure(elements.getTypeElement("java.util.Set").asType()); - this.collectionErasure = - types.erasure(elements.getTypeElement("java.util.Collection").asType()); - this.queueErasure = types.erasure(elements.getTypeElement("java.util.Queue").asType()); - this.linkedListErasure = - types.erasure(elements.getTypeElement("java.util.LinkedList").asType()); - this.iteratorErasure = types.erasure(elements.getTypeElement("java.util.Iterator").asType()); - this.listIteratorErasure = - types.erasure(elements.getTypeElement("java.util.ListIterator").asType()); - - // Initialize annotation mirrors after the hierarchy is established. + this.setErasure = erasureOf("java.util.Set"); + this.collectionErasure = erasureOf("java.util.Collection"); + this.queueErasure = erasureOf("java.util.Queue"); + this.linkedListErasure = erasureOf("java.util.LinkedList"); this.MAYBE_REPLACEABLE = AnnotationBuilder.fromClass(elements, MaybeReplaceable.class); this.REPLACEABLE = AnnotationBuilder.fromClass(elements, Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(elements, Unreplaceable.class); this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(elements, PolyReplaceable.class); - this.topAnnotation = MAYBE_REPLACEABLE; - postInit(); } + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_REPLACEABLE; + } + @Override protected AnnotationMirror positiveCapability() { return REPLACEABLE; @@ -119,79 +96,29 @@ protected Set> createSupportedTypeQualifiers() { } /** - * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for - * aliases whose meaning depends on the annotated type. - * - *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types - * that cannot replace structurally, such as exact {@code Collection}, {@code Set}, non-{@code - * LinkedList} {@code Queue}, and non-{@code ListIterator} {@code Iterator}, their replace - * component canonicalizes to {@code @MaybeReplaceable}. - * - *

{@code @PolyModifiable} remains {@code @PolyReplaceable}: unlike grow and shrink for {@code - * Map.Entry}, replacement through {@code Entry.setValue} is a meaningful capability to carry from - * the map receiver. - * - *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural - * weakening is applied. + * An exact {@code Collection}, a {@code Set}, a non-{@code LinkedList} {@code Queue}, and a + * non-{@code ListIterator} {@code Iterator} have no replace methods, so {@code @Modifiable} and + * {@code @Unmodifiable} make no claim about them. */ @Override - public AnnotationMirror canonicalAnnotation( - AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACEABLE : REPLACEABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotReplace(tm) ? MAYBE_REPLACEABLE : UNREPLACEABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_REPLACEABLE; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return POLY_REPLACEABLE; - } - return super.canonicalAnnotation(annotation); - } - - @Override - public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { - return canonicalAnnotation(annotation, null); - } - - @Override - protected ParameterizedExecutableType methodFromUse( - MethodInvocationTree tree, boolean inferTypeArgs) { - ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); - AnnotatedExecutableType method = mType.executableType(); - - // if the method is listIterator(). - TypeMirror returnUnderlying = method.getReturnType().getUnderlyingType(); - if (TypesUtils.isErasedSubtype(returnUnderlying, listIteratorErasure, types)) { - refineIteratorReturnType(tree, method); - } - - // if the method is annotated @PreservesModifiability - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { - refinePreservesModifiabilityReturnType(tree, method); - } - - return mType; - } - - /** - * Returns true if {@code type} structurally cannot support replace operations. - * - * @param type the type to test - * @return true if {@code type} structurally cannot support replace operations - */ - private boolean typeCannotReplace(TypeMirror type) { + protected boolean typeLacksCapability(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { return false; } - TypeMirror erasedType = types.erasure(type); - return types.isSameType(erasedType, collectionErasure) + return types.isSameType(types.erasure(type), collectionErasure) || TypesUtils.isErasedSubtype(type, setErasure, types) || (TypesUtils.isErasedSubtype(type, queueErasure, types) && !TypesUtils.isErasedSubtype(type, linkedListErasure, types)) || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } + + // polyLacksCapability is not overridden: unlike grow and shrink for Map.Entry, replacement + // through Entry.setValue is a meaningful capability to carry from the map receiver. + + @Override + protected @Nullable TypeMirror refinedIteratorResultBound() { + // A plain Iterator cannot replace, so only a ListIterator result is refined. + return listIteratorErasure; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java deleted file mode 100644 index edc39049f33d..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceVisitor.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.checkerframework.checker.modifiability.replace; - -import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; -import org.checkerframework.common.basetype.BaseTypeChecker; - -/** Visitor for the {@link ReplaceChecker}. */ -public class ReplaceVisitor extends ModifiabilityBaseVisitor { - - /** - * Creates a visitor for the Replace Checker. - * - * @param checker the Replace Checker - */ - public ReplaceVisitor(BaseTypeChecker checker) { - super(checker); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 6548d0df31e6..0f91723bf47d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -1,41 +1,31 @@ package org.checkerframework.checker.modifiability.seqgrow; -import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomSeqGrowable; -import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; -import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolySeqGrowable; -import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.SeqGrowable; import org.checkerframework.checker.modifiability.qual.SeqUngrowable; -import org.checkerframework.checker.modifiability.qual.Unmodifiable; -import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link SeqGrowChecker}. */ public class SeqGrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { - /** The erased {@code java.util.SequencedCollection} type. */ + /** The erased {@code java.util.SequencedCollection} type, or null on a JDK before Java 21. */ private final @Nullable TypeMirror sequencedCollectionErasure; - /** The erased {@code java.util.SequencedMap} type. */ + /** The erased {@code java.util.SequencedMap} type, or null on a JDK before Java 21. */ private final @Nullable TypeMirror sequencedMapErasure; /** The erased {@code java.util.Deque} type. */ @@ -61,26 +51,32 @@ public class SeqGrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeF @SuppressWarnings("this-escape") public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - - TypeElement sequencedCollectionElement = - elements.getTypeElement("java.util.SequencedCollection"); - this.sequencedCollectionErasure = - sequencedCollectionElement == null - ? null - : types.erasure(sequencedCollectionElement.asType()); - TypeElement sequencedMapElement = elements.getTypeElement("java.util.SequencedMap"); - this.sequencedMapErasure = - sequencedMapElement == null ? null : types.erasure(sequencedMapElement.asType()); - this.dequeErasure = types.erasure(elements.getTypeElement("java.util.Deque").asType()); + this.sequencedCollectionErasure = optionalErasureOf("java.util.SequencedCollection"); + this.sequencedMapErasure = optionalErasureOf("java.util.SequencedMap"); + this.dequeErasure = erasureOf("java.util.Deque"); this.MAYBE_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, MaybeSeqGrowable.class); this.SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, SeqGrowable.class); this.SEQ_UNGROWABLE = AnnotationBuilder.fromClass(elements, SeqUngrowable.class); this.POLY_SEQ_GROWABLE = AnnotationBuilder.fromClass(elements, PolySeqGrowable.class); - this.topAnnotation = MAYBE_SEQ_GROWABLE; - postInit(); } + /** + * Returns the erasure of the named type, or null if the type is not present on this JDK. + * + * @param canonicalName the canonical name of a type that may not be present + * @return the erasure of the named type, or null + */ + private @Nullable TypeMirror optionalErasureOf(String canonicalName) { + TypeElement element = elements.getTypeElement(canonicalName); + return element == null ? null : types.erasure(element.asType()); + } + + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_SEQ_GROWABLE; + } + @Override protected AnnotationMirror positiveCapability() { return SEQ_GROWABLE; @@ -108,70 +104,24 @@ protected Set> createSupportedTypeQualifiers() { } /** - * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for - * aliases whose meaning depends on the annotated type. + * Only sequenced collections and sequenced maps have sequenced-grow methods, so + * {@code @Modifiable} and {@code @Unmodifiable} make no claim about any other type. This is an + * allowlist, unlike the blocklists of the other capabilities. * - *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on types - * that structurally cannot support sequenced grow operations, such as {@code Iterator}, their - * SeqGrow component canonicalizes to {@code @MaybeSeqGrowable}. - * - *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural - * weakening is applied. + *

On JDKs before Java 21, {@code SequencedCollection} and {@code SequencedMap} are not + * present, but {@code Deque} still has first/last insertion methods. */ @Override - public AnnotationMirror canonicalAnnotation( - AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm == null || typeCanSeqGrow(tm) ? SEQ_GROWABLE : MAYBE_SEQ_GROWABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm == null || typeCanSeqGrow(tm) ? SEQ_UNGROWABLE : MAYBE_SEQ_GROWABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_SEQ_GROWABLE; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return POLY_SEQ_GROWABLE; - } - return super.canonicalAnnotation(annotation); - } - - @Override - public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { - return canonicalAnnotation(annotation, null); - } - - @Override - protected ParameterizedExecutableType methodFromUse( - MethodInvocationTree tree, boolean inferTypeArgs) { - ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); - AnnotatedExecutableType method = mType.executableType(); - - // if the method is annotated @PreservesModifiability - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { - refinePreservesModifiabilityReturnType(tree, method); - } - - return mType; - } - - /** - * Returns true if {@code type} structurally can support sequenced grow operations. - * - *

Only sequenced collections and sequenced maps can support sequenced grow operations. On JDKs - * before Java 21, {@code SequencedCollection} and {@code SequencedMap} are not present, but - * {@code Deque} still supports first/last insertion operations. - * - * @param type the type to test - * @return true if {@code type} structurally can support sequenced grow operations - */ - private boolean typeCanSeqGrow(TypeMirror type) { + protected boolean typeLacksCapability(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { - return false; + return true; } - return (sequencedCollectionErasure != null - && TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types)) - || (sequencedMapErasure != null - && TypesUtils.isErasedSubtype(type, sequencedMapErasure, types)) - || TypesUtils.isErasedSubtype(type, dequeErasure, types); + boolean canSeqGrow = + (sequencedCollectionErasure != null + && TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types)) + || (sequencedMapErasure != null + && TypesUtils.isErasedSubtype(type, sequencedMapErasure, types)) + || TypesUtils.isErasedSubtype(type, dequeErasure, types); + return !canSeqGrow; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java index 989ad67703c7..8d4ec87eef5d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java @@ -25,4 +25,10 @@ public class SeqGrowChecker extends ModifiabilityBaseChecker { /** Creates a SeqGrowChecker. */ public SeqGrowChecker() {} + + @Override + protected boolean usesIteratorChecker() { + // An iterator has no sequenced-grow methods, so this checker never refines an iterator result. + return false; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java deleted file mode 100644 index 3556138a6996..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowVisitor.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.checkerframework.checker.modifiability.seqgrow; - -import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; -import org.checkerframework.common.basetype.BaseTypeChecker; - -/** Visitor for the {@link SeqGrowChecker}. */ -public class SeqGrowVisitor extends ModifiabilityBaseVisitor { - - /** - * Creates a visitor for the SeqGrow Checker. - * - * @param checker the SeqGrow Checker - */ - public SeqGrowVisitor(BaseTypeChecker checker) { - super(checker); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index 0cba6e40e4c9..ca68a9c9db12 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -1,43 +1,26 @@ package org.checkerframework.checker.modifiability.shrink; -import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomShrinkable; -import org.checkerframework.checker.modifiability.qual.MaybeModifiable; import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; -import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.PolyModifiable; import org.checkerframework.checker.modifiability.qual.PolyShrinkable; -import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.Shrinkable; -import org.checkerframework.checker.modifiability.qual.Unmodifiable; -import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.modifiability.qual.Unshrinkable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the {@link ShrinkChecker}. */ public class ShrinkAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { - /** The erased {@code java.util.Map.Entry} type. */ - private final TypeMirror mapEntryErasure; - - /** The erased {@code java.util.Iterator} type. */ - private final TypeMirror iteratorErasure; - - // -- Hierarchy qualifiers ---------- - /** The {@code @}{@link MaybeShrinkable} qualifier. */ private final AnnotationMirror MAYBE_SHRINKABLE; @@ -58,19 +41,18 @@ public class ShrinkAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFa @SuppressWarnings("this-escape") public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - - this.mapEntryErasure = types.erasure(elements.getTypeElement("java.util.Map.Entry").asType()); - this.iteratorErasure = types.erasure(elements.getTypeElement("java.util.Iterator").asType()); - // Initialize annotation mirrors after the hierarchy is established. this.MAYBE_SHRINKABLE = AnnotationBuilder.fromClass(elements, MaybeShrinkable.class); this.SHRINKABLE = AnnotationBuilder.fromClass(elements, Shrinkable.class); this.UNSHRINKABLE = AnnotationBuilder.fromClass(elements, Unshrinkable.class); this.POLY_SHRINKABLE = AnnotationBuilder.fromClass(elements, PolyShrinkable.class); - this.topAnnotation = MAYBE_SHRINKABLE; - postInit(); } + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_SHRINKABLE; + } + @Override protected AnnotationMirror positiveCapability() { return SHRINKABLE; @@ -98,71 +80,29 @@ protected Set> createSupportedTypeQualifiers() { } /** - * Expands whole-modifiability aliases into this hierarchy, with structural weakening only for - * aliases whose meaning depends on the annotated type. - * - *

{@code @Modifiable} and {@code @Unmodifiable} claim all component capabilities, so on {@code - * Map.Entry}, which cannot shrink, their shrink component canonicalizes to - * {@code @MaybeShrinkable}. - * - *

{@code @PolyModifiable} is different: it should usually become {@code @PolyShrinkable}, but - * for {@code Map.Entry} only the replace bit is meaningful to carry from the map receiver. Its - * shrink bit is therefore {@code @MaybeShrinkable}. - * - *

When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural - * weakening is applied. + * {@code Map.Entry} has no shrink methods, so {@code @Modifiable} and {@code @Unmodifiable} make + * no claim about it. */ @Override - public AnnotationMirror canonicalAnnotation( - AnnotationMirror annotation, @Nullable TypeMirror tm) { - if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINKABLE : SHRINKABLE; - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeCannotShrink(tm) ? MAYBE_SHRINKABLE : UNSHRINKABLE; - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && TypesUtils.isErasedSubtype(tm, mapEntryErasure, types) - ? MAYBE_SHRINKABLE - : POLY_SHRINKABLE; - } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodifiableParam.class)) { - return MAYBE_SHRINKABLE; + protected boolean typeLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; } - return super.canonicalAnnotation(annotation); + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } + /** + * For {@code Map.Entry}, only the replace bit is meaningful to carry from the map receiver, so + * the shrink bit of {@code @PolyModifiable} is {@code @MaybeShrinkable}. + */ @Override - public AnnotationMirror canonicalAnnotation(AnnotationMirror annotation) { - return canonicalAnnotation(annotation, null); + protected boolean polyLacksCapability(TypeMirror type) { + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } @Override - protected ParameterizedExecutableType methodFromUse( - MethodInvocationTree tree, boolean inferTypeArgs) { - ParameterizedExecutableType mType = super.methodFromUse(tree, inferTypeArgs); - AnnotatedExecutableType method = mType.executableType(); - - // if the method is iterator() (including listIterator()). - TypeMirror returnUnderlying = method.getReturnType().getUnderlyingType(); - if (TypesUtils.isErasedSubtype(returnUnderlying, iteratorErasure, types)) { - refineIteratorReturnType(tree, method); - } - - // if the method is annotated @PreservesModifiability - ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { - refinePreservesModifiabilityReturnType(tree, method); - } - - return mType; - } - - /** - * Returns true if {@code type} structurally cannot support shrink operations. - * - * @param type the type to test - * @return true if {@code type} structurally cannot support shrink operations - */ - private boolean typeCannotShrink(TypeMirror type) { - return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); + protected @Nullable TypeMirror refinedIteratorResultBound() { + // Every Iterator can shrink, via remove(). + return iteratorErasure; } } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java deleted file mode 100644 index e828cd5d29c2..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkVisitor.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.checkerframework.checker.modifiability.shrink; - -import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; -import org.checkerframework.common.basetype.BaseTypeChecker; - -/** Visitor for the {@link ShrinkChecker}. */ -public class ShrinkVisitor extends ModifiabilityBaseVisitor { - - /** - * Create a visitor for the Shrink Checker. - * - * @param checker the Shrink Checker - */ - public ShrinkVisitor(BaseTypeChecker checker) { - super(checker); - } -} diff --git a/checker/tests/modifiability/ConstructorResultTypeTest.java b/checker/tests/modifiability/ConstructorResultTypeTest.java new file mode 100644 index 000000000000..53e245e15c0a --- /dev/null +++ b/checker/tests/modifiability/ConstructorResultTypeTest.java @@ -0,0 +1,63 @@ +// Tests the `inconsistent.constructor.result.type` and `bottom.annotation.on.receiver` checks, +// which are performed by ModifiabilityBaseVisitor.processClassTree. + +import java.util.AbstractList; +import org.checkerframework.checker.modifiability.qual.BottomGrowable; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; + +public class ConstructorResultTypeTest { + + // All the constructors declare the same result qualifier, so there is no error. + static class ConsistentConstructors extends AbstractList { + @Growable ConsistentConstructors() {} + + @Growable ConsistentConstructors(int capacity) {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + } + + // The second constructor disagrees with the first. + static class InconsistentConstructors extends AbstractList { + @Growable InconsistentConstructors() {} + + // :: error: [inconsistent.constructor.result.type] + @Ungrowable InconsistentConstructors(int capacity) {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + } + + // The bottom qualifier is meaningless on a receiver, so writing it is an error. + static class BottomReceiver extends AbstractList { + @Growable BottomReceiver() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + // :: error: [bottom.annotation.on.receiver] + public void bottomReceiver(@BottomGrowable BottomReceiver this) {} + } +} diff --git a/checker/tests/modifiability/IdentityHashMapModifiableTest.java b/checker/tests/modifiability/IdentityHashMapModifiableTest.java index 533e39b76e71..97dc24a9dcb4 100644 --- a/checker/tests/modifiability/IdentityHashMapModifiableTest.java +++ b/checker/tests/modifiability/IdentityHashMapModifiableTest.java @@ -14,25 +14,8 @@ void testIdentityHashMap() { boolean removed = identityMap.remove("key", "value2"); } - // this type rule is not implemented yet - // void testViews() { - // @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); - - // // Views (keySet, values, entrySet) usually throw UOE on add operations - // // :: error: [method.invocation] - // identityMap.keySet().add("newKey"); - - // // :: error: [method.invocation] - // identityMap.values().add("newValue"); - - // // :: error: [method.invocation] - // identityMap.entrySet().add(new AbstractMap.SimpleEntry<>("newKey", "newValue")); - - // // However, removal is supported, so these should be allowed: - // identityMap.keySet().remove("key"); - // identityMap.values().remove("value"); - // identityMap.entrySet().remove(new AbstractMap.SimpleEntry<>("key", "value")); - // } + // TODO: The checker does not yet know that a map's keySet(), values(), and entrySet() views + // support removal but not addition. void testEntries() { @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); @@ -49,9 +32,9 @@ void testEntries() { // Stream/Spliterator returns immutable entries if (!identityMap.isEmpty()) { Map.Entry entry = identityMap.entrySet().stream().findFirst().get(); - // This throws UOE at runtime, but we could only make entrySet.iterator() and - // entrySet.stream() return the - // same mutability. So this is potentially unsound. + // TODO: This throws UnsupportedOperationException at run time, but the checker gives + // entrySet().stream() the same modifiability as entrySet().iterator(), so the checker is + // unsound here. entry.setValue("modified"); } } diff --git a/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java b/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java index c5978c410158..bba905fa38c7 100644 --- a/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java +++ b/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java @@ -1,3 +1,8 @@ +// A regression test: an iterator() override whose result carries an alias annotation +// (@Unmodifiable) that expands differently in each hierarchy must not crash the checker, and must +// not conflict with the qualifiers that refineIteratorReturnType would otherwise infer. The +// companion file IteratorConflictingAnnosUse.java calls this method. + import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; diff --git a/checker/tests/modifiability/IteratorConflictingAnnosUse.java b/checker/tests/modifiability/IteratorConflictingAnnosUse.java index dad83b143ea3..56775b7ce8e9 100644 --- a/checker/tests/modifiability/IteratorConflictingAnnosUse.java +++ b/checker/tests/modifiability/IteratorConflictingAnnosUse.java @@ -1,3 +1,6 @@ +// A regression test; see IteratorConflictingAnnosTypeTuple.java, which declares the iterator() +// method that this file calls. + import java.util.Iterator; import org.checkerframework.checker.modifiability.qual.Unmodifiable; diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index 4febfddcfbf4..a1ef0f8935ff 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -71,12 +71,12 @@ void copyOnWriteArrayListIterator() { // store CopyOnWriteArrayList as a list, the iterator is @MaybeShrinkable. List list2 = new CopyOnWriteArrayList<>(); - // TODO!!!!: below the Iterator is default to be unknown shrink because the logic goes: - // if the current iterator return unknown (which is what List.iterator() returns), - // then if the receiver is @Shrinkable and has @IteratorPolyMod, then the result is - // @Shrinkable. - // otherwise, the result is unknown. - // think: should we add a special case for CopyOnWriteArrayList to return unshrinkable iterator? + // The iterator below is @MaybeShrinkable. List.iterator() declares no shrink qualifier, so + // refineIteratorReturnType uses the receiver: a @Shrinkable receiver that is also + // @IteratorPolyMod yields a @Shrinkable result, and every other receiver yields the top + // qualifier. Here the receiver's static type is List, which is not @IteratorPolyMod. + // TODO: Consider giving CopyOnWriteArrayList an @Unshrinkable iterator, so that the result is + // @Unshrinkable rather than @MaybeShrinkable even through a List-typed reference. Iterator iterator2 = list2.iterator(); // :: error: [method.invocation] iterator2.remove(); diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index ba5323d5ff5f..8941b797aed7 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -14,12 +14,14 @@ /** * Tests @PolyModifiable, which expands - * to @PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable @PolyIteratorPolyMod and thus - * preserves all five capabilities independently. + * to @PolyGrowable @PolySeqGrowable @PolyShrinkable @PolyReplaceable and thus preserves all four + * capabilities independently. The alias does not expand into the Iterator hierarchy, so a method + * that must also preserve @IteratorPolyMod writes @PolyIteratorPolyMod explicitly, as `identity` + * below does. */ public class PolyModifiableTest { - /** A simple polymorphic identity method that preserves all four capabilities. */ + /** A simple polymorphic identity method that preserves all five hierarchies. */ @PolyModifiable @PolyIteratorPolyMod List identity(@PolyModifiable @PolyIteratorPolyMod List x) { return x; } diff --git a/checker/tests/modifiability/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java index 6a42f067ce77..59dce03a3437 100644 --- a/checker/tests/modifiability/SeqGrowableTest.java +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -21,7 +21,7 @@ public class SeqGrowableTest { - // :: error: declaration.inconsistent.with.implements.clause + // :: error: [declaration.inconsistent.with.implements.clause] abstract static @SeqGrowable class UserDefinedSortedSet implements SortedSet {} void constructorPositiveCases() { diff --git a/checker/tests/modifiability/UoeImplementationTest.java b/checker/tests/modifiability/UoeImplementationTest.java index a7757b7935d9..e1a2a9e579d4 100644 --- a/checker/tests/modifiability/UoeImplementationTest.java +++ b/checker/tests/modifiability/UoeImplementationTest.java @@ -29,7 +29,7 @@ public void growThrows(@Growable UngrowableList this) { throw new UnsupportedOperationException(); } - // :: error: (method.implementation.not.uoe) + // :: error: [method.implementation.not.uoe] public void growDoesNotThrow(@Growable UngrowableList this) {} } @@ -50,7 +50,7 @@ public int size() { public void growDoesNotThrow(@Growable GrowableList this) {} - // :: error: (method.implementation.is.uoe) + // :: error: [method.implementation.is.uoe] public void growThrows(@Growable GrowableList this) { throw new UnsupportedOperationException(); } @@ -82,7 +82,7 @@ public void seqGrowThrows(@SeqGrowable SeqUngrowableList this) { throw new UnsupportedOperationException(); } - // :: error: (method.implementation.not.uoe) + // :: error: [method.implementation.not.uoe] public void seqGrowDoesNotThrow(@SeqGrowable SeqUngrowableList this) {} } @@ -101,7 +101,7 @@ public int size() { public void seqGrowDoesNotThrow(@SeqGrowable SeqGrowableList this) {} - // :: error: (method.implementation.is.uoe) + // :: error: [method.implementation.is.uoe] public void seqGrowThrows(@SeqGrowable SeqGrowableList this) { throw new UnsupportedOperationException(); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 01ba7f3dcb56..64b7e209d0c9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -76,6 +76,10 @@ In `TypeHierarchy`: ### User-visible changes +The new Modifiability Checker warns when a program might throw +`UnsupportedOperationException` at run time because it calls a mutating method on +a collection that does not support the method. + New `wpi2.sh` script does whole-program inference, but requires buildfile edits. Removed deprecated script `checker/bin-devel/build.sh`; use `./gradlew assemble` diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 944a7f71cc06..9d29e8531cbf 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -1008,7 +1008,9 @@ (see \chapterpageref{constant-value-checker}) \item \ahrefloc{modifiability-checker}{Modifiability Checker} to - warn about possible \ at run time on mutatable collections. (see \chapterpageref{modifiability-checker}) + warn about a possible \ at run time when + modifying a collection + (see \chapterpageref{modifiability-checker}) \end{itemize} diff --git a/docs/manual/inference.tex b/docs/manual/inference.tex index b618edc39abd..a43d15ca36e4 100644 --- a/docs/manual/inference.tex +++ b/docs/manual/inference.tex @@ -183,8 +183,8 @@ \end{Verbatim} The \ task is needed because \ runs the Checker Framework -repeatedly, and Gradle will skip compilation if do nothing if the old (but still -up-to-date) compilation output is present. +repeatedly, and Gradle does nothing if the old (but still up-to-date) +compilation output is present. The output of \ is a top-level directory \ that contains \<.ajava> files for each file of diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 16200a98302a..787aa1f6b8ce 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -98,7 +98,8 @@ (see \chapterpageref{signedness-checker}) \item \ahrefloc{modifiability-checker}{Modifiability Checker} to - warn about possible \ at run time on mutatable collections. + warn about a possible \ at run time when + modifying a collection (see \chapterpageref{modifiability-checker}) \item \ahrefloc{purity-checker}{Purity Checker} to identify whether diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 880e2f45c32f..71108a969d28 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -10,7 +10,6 @@ (It is possible to write a checker that warns about other unsupported methods, but the Modifiability Checker focuses on those issued when modifying collections.) -%% (Optional methods are unrelated to the \ type.) [[ I would just remove this sentence it is distracting.]] An example of an optional method is \sunjavadoc{java.base/java/util/Collection.html\#add(E)}{add}. Some subclasses of \ support \, and others (such as the @@ -82,7 +81,7 @@ support sequential grow operations (like \ and \). The declaration of \ is annotated as \<@Growable @SeqUngrowable>. - ``Sequential Grow'' is shorted to ``SeqGrow'' in the rest of the manual. + ``Sequential Grow'' is shortened to ``SeqGrow'' in the rest of the manual. \item \textbf{Shrink}: The collection can shrink by removing existing elements (\, \, \, \, \, \, \ldots). \item \textbf{Replace}: The collection can replace existing @@ -147,8 +146,9 @@ The Modifiability Checker uses \textbf{multiple independent 4-element hierarchies}, one per capability. Every collection type carries exactly one qualifier from each hierarchy. The checker also uses an additional -2-element hierarchy to express whether \ can be called on -a collection's iterator. +2-element hierarchy to express whether a collection's iterator preserves the +collection's capabilities: whether \ can be called on the iterator, +and, for a \, whether \ and \ can be called. \begin{figure} \begin{smaller} @@ -609,7 +609,7 @@ it3.remove(); // Error: list3 does not have @IteratorPolyMod \end{Verbatim} -\sectionAndLabel{Unverified collection implementation annotations}{class.unverified} +\sectionAndLabel{Unverified collection implementation annotations}{modifiability-class-unverified} The Modifiability Checker verifies uses of collection modifiability annotations, but it does not verify collection implementations. diff --git a/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java b/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java index 09871fe410a0..38ce78a9e2c6 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/RemoveAnnotationsForInference.java @@ -291,7 +291,6 @@ static void removeAnnotations(Path absolutePath, List removals) * @param lineno the index of the line to be removed or replaced * @param newLine the new line for index {@code lineno} */ - @SuppressWarnings("deprecation") // JDK's String.isBlank() only exists on JDK 11+. static void replaceLine(List lines, int lineno, String newLine) { if (newLine.isBlank()) { lines.remove(lineno); From e171765ba5ae4b248dfc5ecf1a62573feb961d76 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 5 Sep 2026 20:23:27 -0700 Subject: [PATCH 277/308] Add signature annotations --- .../modifiability/ModifiabilityBaseAnnotatedTypeFactory.java | 3 ++- .../modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 207ce44e492a..26303e5a2119 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -16,6 +16,7 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -69,7 +70,7 @@ protected ModifiabilityBaseAnnotatedTypeFactory(BaseTypeChecker checker) { * @param canonicalName the canonical name of a type that is always present * @return the erasure of the named type */ - protected final TypeMirror erasureOf(String canonicalName) { + protected final TypeMirror erasureOf(@CanonicalName String canonicalName) { return types.erasure(elements.getTypeElement(canonicalName).asType()); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 0f91723bf47d..30902c03548e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -15,6 +15,7 @@ import org.checkerframework.checker.modifiability.qual.SeqGrowable; import org.checkerframework.checker.modifiability.qual.SeqUngrowable; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TypesUtils; @@ -67,7 +68,7 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { * @param canonicalName the canonical name of a type that may not be present * @return the erasure of the named type, or null */ - private @Nullable TypeMirror optionalErasureOf(String canonicalName) { + private @Nullable TypeMirror optionalErasureOf(@CanonicalName String canonicalName) { TypeElement element = elements.getTypeElement(canonicalName); return element == null ? null : types.erasure(element.asType()); } From 0f1a37e3dff59dd0ca4fc44eafa55bdaa08722c3 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 12 Sep 2026 15:29:58 -0700 Subject: [PATCH 278/308] Code review fixes for the Modifiability Checker * Remove the write-only static `classWarned` set, which never suppressed a duplicate error and accumulated across compilation tasks. * Check a method body against the constructor annotation only if the method declares a receiver parameter. Otherwise the receiver qualifier was defaulted, often from a qualifier on the class declaration, and says nothing about whether the method is one of the capability's operations. * Document why `checkThisOrSuperConstructorCall()` is suppressed, and make the "check inherited methods" TODO accurate. * `@PreservesModifiability` no longer overwrites a declared non-top result qualifier, which could widen a declared negative result to positive. * Narrow a declared positive iterator result when the receiver has the negative qualifier. * Remove the dead polymorphic-qualifier test in `refineIteratorReturnType()`; `super.methodFromUse()` has already resolved polymorphism. * Comment out the manual section for the unimplemented `class.unverified` warning. * Say in build.gradle that `-Awarns` makes `checkModifiability` enforce nothing. Co-Authored-By: Claude Opus 5 --- build.gradle | 4 ++ ...ModifiabilityBaseAnnotatedTypeFactory.java | 43 +++++++++---- .../ModifiabilityBaseVisitor.java | 60 ++++++++++--------- docs/manual/modifiability-checker.tex | 52 ++++++++-------- 4 files changed, 93 insertions(+), 66 deletions(-) diff --git a/build.gradle b/build.gradle index 1a9402fab8b3..4896d6ffe428 100644 --- a/build.gradle +++ b/build.gradle @@ -978,6 +978,10 @@ subprojects { rootProject.createCheckTypeTask(project.name, "Modifiability", "org.checkerframework.checker.modifiability.ModifiabilityChecker", [ + // The Modifiability Checker is under development and this codebase is not yet free of + // its errors, so `-Awarns` downgrades them to warnings. This means that the + // `checkModifiability` task always succeeds; it does not enforce anything. Remove + // `-Awarns` when the codebase issues no modifiability errors. "-Awarns", "-Afilenames", ]) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 26303e5a2119..212e5ff22daa 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -22,6 +22,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -231,9 +232,10 @@ protected ParameterizedExecutableType methodFromUse( * *

If the method has no parameters or returns {@code void}, then the annotation has no effect. * - *

Otherwise, if the first argument has this checker's positive qualifier (for example, - * {@code @Shrinkable}), then so does the return type. For every other first argument, the return - * type is the top qualifier. + *

Otherwise, if the declared return type has a qualifier other than the top qualifier, that + * declared qualifier is used. If the first argument has this checker's positive qualifier (for + * example, {@code @Shrinkable}), then so does the return type. For every other first argument, + * the return type is the top qualifier. * *

Such a method cannot be annotated as {@code @Poly*}, because a negative (for example, * {@code @Unshrinkable}) input could yield either a positive or a negative result. It would be @@ -252,6 +254,14 @@ protected void refinePreservesModifiabilityReturnType( || returnType.getUnderlyingType().getKind() == TypeKind.VOID) { return; } + AnnotationMirror declaredReturnAnno = + returnType.getPrimaryAnnotationInHierarchy(topAnnotation()); + if (declaredReturnAnno != null + && !AnnotationUtils.areSameByName(declaredReturnAnno, topAnnotation())) { + // The declared result states its own qualifier, which is more precise (or, for a negative + // qualifier, a stronger guarantee) than what the argument implies. + return; + } AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); if (argumentType.hasPrimaryAnnotation(positiveCapability())) { returnType.replaceAnnotation(positiveCapability()); @@ -269,11 +279,13 @@ protected void refinePreservesModifiabilityReturnType( * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, * special treatment is needed for iterator methods. * - *

An explicitly declared result keeps its declared qualifier. Otherwise, the iterator of a - * receiver with this checker's negative qualifier also has that negative qualifier, and the - * iterator of a receiver that has both this checker's positive qualifier and + *

The iterator of a receiver with this checker's negative qualifier also has that negative + * qualifier; this is true even of a declaration such as {@code ArrayList}'s {@code @Growable + * ListIterator listIterator()}, whose result has the capability only when the receiver does. A + * declared negative result keeps its declared qualifier, since such an iterator never has the + * capability. The iterator of a receiver that has both this checker's positive qualifier and * {@code @IteratorPolyMod} has the positive qualifier. In every other case the declared result - * type is left alone, which leaves the top qualifier in place. + * type is left alone. * *

This method is called by the Grow, Shrink, and Replace Checkers; see {@link * #refinedIteratorResultBound}. @@ -293,13 +305,12 @@ protected void refineIteratorReturnType( return; } AnnotatedTypeMirror returnType = methodType.getReturnType(); - // Keep explicit ungrowable/growable iterator contracts (for example, CopyOnWriteArrayList, - // ArrayList). - if (returnType.hasPrimaryAnnotation(negativeCapability()) - || returnType.hasPrimaryAnnotation(positiveCapability()) - || returnType.hasPrimaryAnnotation(polyCapability())) { + // Keep an explicit "no capability" iterator contract (for example, CopyOnWriteArrayList). + if (returnType.hasPrimaryAnnotation(negativeCapability())) { return; } + // There is no need to test for the polymorphic qualifier: `super.methodFromUse()` has already + // resolved it. Tree receiverTree = TreeUtils.getReceiverTree(tree); if (receiverTree == null) { @@ -307,12 +318,18 @@ protected void refineIteratorReturnType( } AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); - // The iterator of a collection that lacks the capability also lacks the capability. + // The iterator of a collection that lacks the capability also lacks the capability, even if + // the declaration says that the iterator has it (as ArrayList's does). if (receiverType.hasPrimaryAnnotation(negativeCapability())) { returnType.replaceAnnotation(negativeCapability()); return; } + // Keep an explicit "has capability" iterator contract (for example, ArrayList). + if (returnType.hasPrimaryAnnotation(positiveCapability())) { + return; + } + // The receiver has the capability; its iterator does too if the receiver is @IteratorPolyMod. if (receiverType.hasPrimaryAnnotation(positiveCapability())) { AnnotatedTypeMirror iteratorHierarchyType = diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index d5c6480dc2f9..f96d05471cd0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -12,14 +12,11 @@ import com.sun.source.tree.ThrowTree; import com.sun.source.tree.Tree; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; -import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -36,9 +33,12 @@ *

    *
  • Suppressing the "constructor result must be TOP" check, since collection constructors may * legitimately produce {@code @Modifiable}. + *
  • Suppressing the rule that relates a constructor's result to that of the {@code this()} or + * {@code super()} call within it, since a class may declare a different modifiability than + * its superclass does. *
  • Requiring all the constructors of a class to declare the same result qualifier, and - * requiring each method body to agree with that qualifier about whether the method throws - * {@link UnsupportedOperationException}. + * requiring the body of each method that states a receiver requirement to agree with that + * qualifier about whether the method throws {@link UnsupportedOperationException}. *
  • Requiring an override to preserve a positive receiver capability of the method it * overrides. *
@@ -50,9 +50,6 @@ public class ModifiabilityBaseVisitor // private static final String MODIFIABILITY_QUAL_PACKAGE = // "org.checkerframework.checker.modifiability.qual"; - /** Classes for which an error has been issued, to avoid issuing multiple errors. */ - private static final Set<@FullyQualifiedName String> classWarned = new HashSet<>(); - /** * Create a ModifiabilityBaseVisitor. * @@ -62,10 +59,24 @@ public ModifiabilityBaseVisitor(BaseTypeChecker checker) { super(checker); } + /** + * Suppresses the framework's rule that a constructor's result type must be a supertype of the + * result of the {@code this()} or {@code super()} call within it. + * + *

A collection class may legitimately declare a different modifiability than its superclass + * does; for example, a class whose constructors are {@code @Ungrowable} may extend {@code + * AbstractList}, whose constructor is {@code @Growable}, and override every grow method to throw + * {@link UnsupportedOperationException}. The framework's rule would reject every such class, + * including at the implicit {@code super()} call of a constructor that has no explicit one. + * + *

What makes the suppression safe is that the declared modifiability of a class is checked + * against its method bodies; see {@link #processClassConstructors}. That check is not yet + * complete: it does not yet examine methods that the class inherits rather than declares. + */ @Override protected void checkThisOrSuperConstructorCall( MethodInvocationTree call, @CompilerMessageKey String errorKey) { - // Nothing to do; it is handled by `processClassTree()`. + // Do nothing. } @Override @@ -101,7 +112,6 @@ private void processClassConstructors(ClassTree tree) { } } - @FullyQualifiedName String className = classTM.toString(); boolean thisClassWarned = false; AnnotationMirror constructorAnno = null; @@ -119,7 +129,6 @@ private void processClassConstructors(ClassTree tree) { } else if (!AnnotationUtils.areSameByName(thisResultAnno, constructorAnno)) { checker.reportError( constructor, "inconsistent.constructor.result.type", thisResultAnno, constructorAnno); - classWarned.add(className); thisClassWarned = true; } } @@ -131,27 +140,31 @@ private void processClassConstructors(ClassTree tree) { return; } - // There is at least one constructor, and all constructors have the same result type annotation. - // Examine: - // * the implementation of each method - // * the annotation of each inherited method, only if the superclass constructors have - // different type than this class's constructors. + // There is at least one constructor, and all constructors have the same result type + // annotation. Examine the implementation of each method that states a capability requirement. + // TODO: also examine each method that inherits a capability requirement from a method that it + // overrides, such as an override of `Collection.add()`. for (MethodTree method : methods) { if (method.getBody() == null) { // The method is abstract or native, so it has no implementation to check. continue; } + if (method.getReceiverParameter() == null) { + // The method does not state a requirement on its receiver. Its receiver qualifier was + // defaulted -- often from a qualifier on the class declaration, which applies to every + // method of the class -- so it does not indicate that this method is one of the + // operations that this checker's capability is about. + continue; + } AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); if (receiverType != null) { AnnotationMirror receiverAnno = receiverType.getAnnotation(); if (receiverAnno != null) { - checkImplOK(method, receiverAnno, constructorAnno, className); + checkImplOK(method, receiverAnno, constructorAnno); } } } - - // TODO: check for overridden methods, } /** @@ -160,13 +173,9 @@ private void processClassConstructors(ClassTree tree) { * @param method a method declaration * @param receiverAnno the annotation on the method's receiver parameter * @param constructorAnno the annotation on the class constructors - * @param className the name of the enclosing class, used only for diagnostic messages */ private void checkImplOK( - MethodTree method, - AnnotationMirror receiverAnno, - AnnotationMirror constructorAnno, - @FullyQualifiedName String className) { + MethodTree method, AnnotationMirror receiverAnno, AnnotationMirror constructorAnno) { // Every modifiability hierarchy contains a top qualifier, a positive qualifier, and a // polymorphic qualifier. Every hierarchy but the Iterator one also contains a negative and a // bottom qualifier. @@ -179,7 +188,6 @@ private void checkImplOK( if (!AnnotationUtils.areSameByName(receiverAnno, positiveCapability())) { // The only qualifier left is the bottom one. checker.reportError(method, "bottom.annotation.on.receiver"); - classWarned.add(className); return; } @@ -196,12 +204,10 @@ private void checkImplOK( if (isNegativeCapability(constructorAnno)) { if (!implIsUOE(method)) { checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); - classWarned.add(className); } } else { if (implIsUOE(method)) { checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); - classWarned.add(className); } } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 71108a969d28..bcfe11c9975e 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -609,32 +609,32 @@ it3.remove(); // Error: list3 does not have @IteratorPolyMod \end{Verbatim} -\sectionAndLabel{Unverified collection implementation annotations}{modifiability-class-unverified} - -The Modifiability Checker verifies uses of collection modifiability annotations, -but it does not verify collection implementations. - -When a user-defined collection-like class (a \, \, -\, or subtype) explicitly writes a concrete modifiability qualifier -on the class declaration or on a constructor, the checker issues a -\ warning. The warning means that the -annotation is a trusted specification of the custom implementation, not a fact -verified from the implementation's method bodies. After examining the -implementation, suppress the warning with -\<@SuppressWarnings("class.unverified")>. - -For example, the class-level \<@Growable> annotation below triggers -\. It is up to the programmer to verify that -the implementation of \ is not \@Growable and remove it from the class declaration. -\begin{Verbatim} -public @Growable class TestMod extends ArrayList { - - @Override - public boolean add(@Growable TestMod this, String s) { - throw new UnsupportedOperationException(); - } -} -\end{Verbatim} +% \sectionAndLabel{Unverified collection implementation annotations}{modifiability-class-unverified} +% +% The Modifiability Checker verifies uses of collection modifiability annotations, +% but it does not verify collection implementations. +% +% When a user-defined collection-like class (a \, \, +% \, or subtype) explicitly writes a concrete modifiability qualifier +% on the class declaration or on a constructor, the checker issues a +% \ warning. The warning means that the +% annotation is a trusted specification of the custom implementation, not a fact +% verified from the implementation's method bodies. After examining the +% implementation, suppress the warning with +% \<@SuppressWarnings("class.unverified")>. +% +% For example, the class-level \<@Growable> annotation below triggers +% \. It is up to the programmer to verify that +% the implementation of \ is not \@Growable and remove it from the class declaration. +% \begin{Verbatim} +% public @Growable class TestMod extends ArrayList { +% +% @Override +% public boolean add(@Growable TestMod this, String s) { +% throw new UnsupportedOperationException(); +% } +% } +% \end{Verbatim} From 52500e544981099e50f15f9095420536032fd45a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 12 Sep 2026 16:55:22 -0700 Subject: [PATCH 279/308] Follow-up code review fixes for the Modifiability Checker * An iterator's declared positive qualifier is no longer kept. It holds only when the receiver has the capability and preserves it, so the result qualifier is now computed from the receiver in every case but a declared negative one. * Restore the polymorphic-qualifier test in `refineIteratorReturnType()`: `super.methodFromUse()` resolves polymorphism only when it infers type arguments, so on the other path the qualifier must be left for later resolution. * Check the body of a method that inherits a capability requirement from a method that it overrides, such as an override of `Collection.add()`. This restores the check for a class whose qualifier is written on the class declaration, which the receiver-parameter test had disabled. * Compare a method body against the constructor annotation only if the constructors claim the capability or its absence. A top or polymorphic constructor result claims neither, so either body is consistent with it. * Say accurately what `checkThisOrSuperConstructorCall()`'s suppression does and does not verify. Co-Authored-By: Claude Opus 5 --- ...ModifiabilityBaseAnnotatedTypeFactory.java | 43 +++++++---- .../ModifiabilityBaseVisitor.java | 77 ++++++++++++++----- .../modifiability/IteratorPrecisionTest.java | 9 +++ .../modifiability/UoeImplementationTest.java | 23 ++++++ 4 files changed, 117 insertions(+), 35 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 212e5ff22daa..1fdfd84b0286 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -279,13 +279,18 @@ protected void refinePreservesModifiabilityReturnType( * CopyOnWriteArrayList} has unmodifiable iterators even though the list is modifiable.) Thus, * special treatment is needed for iterator methods. * - *

The iterator of a receiver with this checker's negative qualifier also has that negative - * qualifier; this is true even of a declaration such as {@code ArrayList}'s {@code @Growable - * ListIterator listIterator()}, whose result has the capability only when the receiver does. A - * declared negative result keeps its declared qualifier, since such an iterator never has the - * capability. The iterator of a receiver that has both this checker's positive qualifier and - * {@code @IteratorPolyMod} has the positive qualifier. In every other case the declared result - * type is left alone. + *

A declared negative result keeps its declared qualifier, since such an iterator never has + * the capability. Otherwise, the result qualifier is computed from the receiver: the iterator of + * a receiver with this checker's negative qualifier also has that negative qualifier, and the + * iterator of a receiver that has both this checker's positive qualifier and + * {@code @IteratorPolyMod} has the positive qualifier. In every other case the result is the top + * qualifier. + * + *

A declared positive result is not kept, because it holds only when the receiver has the + * capability and preserves it. For example, {@code ArrayList} declares {@code @Growable + * ListIterator listIterator()}, but an {@code @Ungrowable ArrayList} has an + * {@code @Ungrowable} list iterator and a {@code @MaybeGrowable ArrayList} has a + * {@code @MaybeGrowable} one. * *

This method is called by the Grow, Shrink, and Replace Checkers; see {@link * #refinedIteratorResultBound}. @@ -305,15 +310,22 @@ protected void refineIteratorReturnType( return; } AnnotatedTypeMirror returnType = methodType.getReturnType(); - // Keep an explicit "no capability" iterator contract (for example, CopyOnWriteArrayList). + // Keep an explicit "no capability" iterator contract (for example, CopyOnWriteArrayList), + // whose iterator lacks the capability no matter what the receiver is. if (returnType.hasPrimaryAnnotation(negativeCapability())) { return; } - // There is no need to test for the polymorphic qualifier: `super.methodFromUse()` has already - // resolved it. + if (returnType.hasPrimaryAnnotation(polyCapability())) { + // `super.methodFromUse()` resolves the polymorphic qualifier only when it infers type + // arguments; see `GenericAnnotatedTypeFactory.methodFromUsePreSubstitution()`. On the other + // path, leave the polymorphic qualifier for that later resolution. + return; + } Tree receiverTree = TreeUtils.getReceiverTree(tree); if (receiverTree == null) { + // TODO: The receiver is the implicit `this`, whose type is the receiver type of the + // enclosing method. Until that is implemented, leave the declared result alone. return; } AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); @@ -325,19 +337,20 @@ protected void refineIteratorReturnType( return; } - // Keep an explicit "has capability" iterator contract (for example, ArrayList). - if (returnType.hasPrimaryAnnotation(positiveCapability())) { - return; - } - // The receiver has the capability; its iterator does too if the receiver is @IteratorPolyMod. if (receiverType.hasPrimaryAnnotation(positiveCapability())) { AnnotatedTypeMirror iteratorHierarchyType = getTypeFactoryOfSubchecker(IteratorChecker.class).getAnnotatedType(receiverTree); if (iteratorHierarchyType.hasPrimaryAnnotation(ITERATOR_POLY_MOD)) { returnType.replaceAnnotation(positiveCapability()); + return; } } + + // The receiver does not both have the capability and preserve it, so its iterator has no + // guarantee about the capability -- even if the declared result says that it does, as + // ArrayList's `@Growable ListIterator listIterator()` does. + returnType.replaceAnnotation(topAnnotation()); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index f96d05471cd0..23b8a9b6884d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -13,15 +13,18 @@ import com.sun.source.tree.Tree; import java.util.ArrayList; import java.util.List; +import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; @@ -37,8 +40,9 @@ * {@code super()} call within it, since a class may declare a different modifiability than * its superclass does. *

  • Requiring all the constructors of a class to declare the same result qualifier, and - * requiring the body of each method that states a receiver requirement to agree with that - * qualifier about whether the method throws {@link UnsupportedOperationException}. + * requiring the body of each method that requires the capability -- either on its own + * receiver parameter or from a method that it overrides -- to agree with that qualifier about + * whether the method throws {@link UnsupportedOperationException}. *
  • Requiring an override to preserve a positive receiver capability of the method it * overrides. * @@ -69,9 +73,11 @@ public ModifiabilityBaseVisitor(BaseTypeChecker checker) { * {@link UnsupportedOperationException}. The framework's rule would reject every such class, * including at the implicit {@code super()} call of a constructor that has no explicit one. * - *

    What makes the suppression safe is that the declared modifiability of a class is checked - * against its method bodies; see {@link #processClassConstructors}. That check is not yet - * complete: it does not yet examine methods that the class inherits rather than declares. + *

    What makes the suppression less unsafe is that the declared modifiability of a class is + * checked against its method bodies; see {@link #processClassConstructors}. That check is not + * complete, so the suppression does permit some unsound code. The check does nothing unless some + * constructor of the class declares a qualifier in this hierarchy, and it examines only the + * methods that the class declares, not those that it inherits without overriding. */ @Override protected void checkThisOrSuperConstructorCall( @@ -141,30 +147,58 @@ private void processClassConstructors(ClassTree tree) { } // There is at least one constructor, and all constructors have the same result type - // annotation. Examine the implementation of each method that states a capability requirement. - // TODO: also examine each method that inherits a capability requirement from a method that it - // overrides, such as an override of `Collection.add()`. + // annotation. Examine the implementation of each method that requires the capability. for (MethodTree method : methods) { if (method.getBody() == null) { // The method is abstract or native, so it has no implementation to check. continue; } - if (method.getReceiverParameter() == null) { - // The method does not state a requirement on its receiver. Its receiver qualifier was - // defaulted -- often from a qualifier on the class declaration, which applies to every - // method of the class -- so it does not indicate that this method is one of the - // operations that this checker's capability is about. - continue; + AnnotationMirror receiverAnno = receiverCapabilityRequirement(method); + if (receiverAnno != null) { + checkImplOK(method, receiverAnno, constructorAnno); } + } + } + + /** + * Returns the qualifier that states what {@code method} requires of its receiver, or null if the + * method states no requirement. + * + *

    A requirement that the method writes on its own receiver parameter is used directly. + * Otherwise, the method's receiver qualifier was defaulted -- often from a qualifier on the class + * declaration, which applies to every method of the class -- so it does not indicate that this + * method is one of the operations that this checker's capability is about. In that case, the + * requirement is the one that the method inherits from the methods it overrides, such as the + * {@code @Growable} receiver of {@code Collection.add()}. + * + * @param method a method declaration + * @return the capability that {@code method} requires of its receiver, or null if none + */ + private @Nullable AnnotationMirror receiverCapabilityRequirement(MethodTree method) { + if (method.getReceiverParameter() != null) { AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); - if (receiverType != null) { - AnnotationMirror receiverAnno = receiverType.getAnnotation(); - if (receiverAnno != null) { - checkImplOK(method, receiverAnno, constructorAnno); - } + return receiverType == null ? null : receiverType.getAnnotation(); + } + + ExecutableElement methodElt = TreeUtils.elementFromDeclaration(method); + if (methodElt == null) { + return null; + } + for (Map.Entry pair : + AnnotatedTypes.overriddenMethods(elements, atypeFactory, methodElt).entrySet()) { + AnnotatedExecutableType overriddenMethodType = + AnnotatedTypes.asMemberOf(types, atypeFactory, pair.getKey(), pair.getValue()); + AnnotatedDeclaredType overriddenReceiver = overriddenMethodType.getReceiverType(); + // Only a positive requirement is inherited. A defaulted top or polymorphic qualifier says + // nothing, and a negative or bottom qualifier is not a requirement that this method must + // live up to. + if (overriddenReceiver != null + && overriddenReceiver.hasPrimaryAnnotation(positiveCapability())) { + return positiveCapability(); } } + return null; } /** @@ -205,11 +239,14 @@ private void checkImplOK( if (!implIsUOE(method)) { checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); } - } else { + } else if (AnnotationUtils.areSameByName(constructorAnno, positiveCapability())) { if (implIsUOE(method)) { checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); } } + // Otherwise, the constructors' result is the top or the polymorphic qualifier, which claims + // neither that the class has the capability nor that it lacks it. Either method body is + // consistent with such a constructor. } /** diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java index a1ef0f8935ff..014dca4b3169 100644 --- a/checker/tests/modifiability/IteratorPrecisionTest.java +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -106,6 +106,15 @@ void arryaListListIterator() { @Modifiable ListIterator indexedIterator2 = list2.listIterator(0); } + // ArrayList declares `@Growable ListIterator listIterator()`, but that result has the + // capability only when the receiver has it. A parameter whose type is written as plain + // `ArrayList` is @MaybeGrowable, so its list iterator is @MaybeGrowable too. + void listIteratorOfMaybeGrowableArrayList(ArrayList list) { + ListIterator iterator = list.listIterator(); + // :: error: [method.invocation] + iterator.add("a"); + } + void UnmodListIterator() { List list = List.of("a", "b"); @Unshrinkable Iterator iterator = list.iterator(); diff --git a/checker/tests/modifiability/UoeImplementationTest.java b/checker/tests/modifiability/UoeImplementationTest.java index e1a2a9e579d4..42f6bfb83906 100644 --- a/checker/tests/modifiability/UoeImplementationTest.java +++ b/checker/tests/modifiability/UoeImplementationTest.java @@ -56,6 +56,29 @@ public void growThrows(@Growable GrowableList this) { } } + // `add()` states no requirement on its own receiver, but it inherits one from `List.add()`, + // which it overrides. All the constructors are @Growable, so `add()` must not throw + // UnsupportedOperationException. + static @Growable class ClassLevelGrowableList extends AbstractList { + @Growable ClassLevelGrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + @Override + // :: error: [method.implementation.is.uoe] + public boolean add(String element) { + throw new UnsupportedOperationException(); + } + } + // A method with no body has no implementation to check. abstract static class AbstractUngrowableList extends AbstractList { @Ungrowable AbstractUngrowableList() {} From 8cb081d2a1ecf203aa7cef4221a7a835c475c9d0 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 13 Sep 2026 08:42:34 -0700 Subject: [PATCH 280/308] Fix the two CI failures on the modifiability branch checkSignature: `erasureOf` and `optionalErasureOf` declared their parameter as `@CanonicalName`, but the Signature Checker types a string literal such as `"java.util.Map.Entry"` as `@DotSeparatedIdentifiers`, which is not a subtype of `@CanonicalName`. Use `@FullyQualifiedName`, which is what `Elements.getTypeElement` itself declares. all-systems/Issue8053: `computeIfAbsent` requires a `@Growable` receiver, and a field's declared type is not refined from its initializer unless the type is immutable, so the `static final` map was `@MaybeGrowable`. Use a local variable, whose type is refined from `new HashMap<>()`. Co-Authored-By: Claude Opus 5 --- .../ModifiabilityBaseAnnotatedTypeFactory.java | 9 +++++++-- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 4 ++-- framework/tests/all-systems/Issue8053.java | 10 ++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 1fdfd84b0286..c57e398e30a0 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -16,7 +16,7 @@ import org.checkerframework.checker.modifiability.qual.Unmodifiable; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.signature.qual.CanonicalName; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -68,10 +68,15 @@ protected ModifiabilityBaseAnnotatedTypeFactory(BaseTypeChecker checker) { /** * Returns the erasure of the named type. * + *

    The parameter is {@code @FullyQualifiedName} rather than {@code @CanonicalName}, which is + * what {@link javax.lang.model.util.Elements#getTypeElement} really requires, because the + * Signature Checker cannot prove that a string literal such as {@code "java.util.Map.Entry"} is a + * canonical name. + * * @param canonicalName the canonical name of a type that is always present * @return the erasure of the named type */ - protected final TypeMirror erasureOf(@CanonicalName String canonicalName) { + protected final TypeMirror erasureOf(@FullyQualifiedName String canonicalName) { return types.erasure(elements.getTypeElement(canonicalName).asType()); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 30902c03548e..0c265bb3e82e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -15,7 +15,7 @@ import org.checkerframework.checker.modifiability.qual.SeqGrowable; import org.checkerframework.checker.modifiability.qual.SeqUngrowable; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.signature.qual.CanonicalName; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TypesUtils; @@ -68,7 +68,7 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { * @param canonicalName the canonical name of a type that may not be present * @return the erasure of the named type, or null */ - private @Nullable TypeMirror optionalErasureOf(@CanonicalName String canonicalName) { + private @Nullable TypeMirror optionalErasureOf(@FullyQualifiedName String canonicalName) { TypeElement element = elements.getTypeElement(canonicalName); return element == null ? null : types.erasure(element.asType()); } diff --git a/framework/tests/all-systems/Issue8053.java b/framework/tests/all-systems/Issue8053.java index 1b1bcade0baf..581f9042e63b 100644 --- a/framework/tests/all-systems/Issue8053.java +++ b/framework/tests/all-systems/Issue8053.java @@ -41,14 +41,16 @@ interface Getter {} return types.stream().map(t -> wild(t)).collect(Collectors.toList()); } - static final Map>> CACHE = new HashMap<>(); - static native

    Getter createGetter(Class

    clazz, String name); // The Beam shape: the whole thing is the mapping function of Map.computeIfAbsent, and the - // Class argument is itself wildcard-typed. + // Class argument is itself wildcard-typed. The map is a local variable rather than a field so + // that a checker that reasons about the receiver of `computeIfAbsent` (such as the Modifiability + // Checker, whose `computeIfAbsent` requires a receiver that permits growing) can refine its type + // from the `new HashMap<>()`; a field's type is not refined that way. static List> beamShape(Class clazz, List types, String key) { - return CACHE.computeIfAbsent( + Map>> cache = new HashMap<>(); + return cache.computeIfAbsent( key, c -> types.stream().map(t -> createGetter(clazz, t)).collect(Collectors.toList())); } From 1f98dc95859b63a2daf91ae478a948a4d9e9475b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 14 Sep 2026 07:42:16 -0700 Subject: [PATCH 281/308] Code review --- .../afu/scenelib/el/AElement.java | 5 ++- checker-qual/src/main/java/module-info.java | 1 + .../qual/PreservesModifiability.java | 6 +++ ...ModifiabilityBaseAnnotatedTypeFactory.java | 33 +++++++++------- .../grow/GrowAnnotatedTypeFactory.java | 3 ++ .../shrink/ShrinkAnnotatedTypeFactory.java | 3 ++ .../modifiability/GenericCapabilityTest.java | 32 ++++++++++++++++ .../ImplicitReceiverIteratorTest.java | 38 +++++++++++++++++++ 8 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 checker/tests/modifiability/GenericCapabilityTest.java create mode 100644 checker/tests/modifiability/ImplicitReceiverIteratorTest.java diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java index a35f16b5df48..8395f5f8b119 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java @@ -245,7 +245,10 @@ public boolean isEmptyValue(AElement v) { */ @SuppressWarnings({ "unchecked", - "nullness:argument" // TODO + // `orig.get(key)` passes a `K`, whose implicit upper bound is `@Nullable Object`, to + // `WrapperMap.get(Object)`, whose parameter is `@NonNull`. This warning appears only when + // this file is nullness-checked, which requires `-PnullnessAll`. + "nullness:argument" }) static void copyMapContents( VivifyingMap orig, @Growable @Replaceable VivifyingMap copy) { diff --git a/checker-qual/src/main/java/module-info.java b/checker-qual/src/main/java/module-info.java index 4c8fab8909dc..f1e80c41a2df 100644 --- a/checker-qual/src/main/java/module-info.java +++ b/checker-qual/src/main/java/module-info.java @@ -23,6 +23,7 @@ exports org.checkerframework.checker.initialization.qual; exports org.checkerframework.checker.interning.qual; exports org.checkerframework.checker.lock.qual; + exports org.checkerframework.checker.modifiability.qual; exports org.checkerframework.checker.mustcall.qual; exports org.checkerframework.checker.nullness.qual; exports org.checkerframework.checker.optional.qual; diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java index c5f414ad7693..224dadcde798 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -24,4 +24,10 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) +// TODO: This annotation is trusted, not verified: no check ensures that the method body actually +// preserves the capabilities of its first argument, so an incorrect use is unsound. It also lacks +// @InheritedAnnotation, so a call through an override that does not repeat the annotation is not +// refined; the type of a call therefore depends on the static type of the receiver. Either verify +// the contract in ModifiabilityBaseVisitor, or make the annotation @InheritedAnnotation and +// document it as trusted. public @interface PreservesModifiability {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index c57e398e30a0..7dbda75293af 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.modifiability; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; @@ -141,7 +140,8 @@ protected boolean expandsModifiabilityAliases() { * {@code @Modifiable} and {@code @Unmodifiable} weaken to the top qualifier on {@code type}. For * example, {@code Map.Entry} cannot grow. * - * @param type the type on which an alias was written + * @param type the type on which an alias was written; it is an upper bound, so it is never a type + * variable or a wildcard * @return true if {@code type} structurally cannot support this checker's capability */ protected boolean typeLacksCapability(TypeMirror type) { @@ -154,7 +154,8 @@ protected boolean typeLacksCapability(TypeMirror type) { * because a polymorphic qualifier may usefully carry a capability that the type itself cannot * exercise; for example, {@code Map.Entry} carries the replace capability of its map. * - * @param type the type on which {@code @PolyModifiable} was written + * @param type the type on which {@code @PolyModifiable} was written; it is an upper bound, so it + * is never a type variable or a wildcard * @return true if {@code @PolyModifiable} weakens to the top qualifier on {@code type} */ protected boolean polyLacksCapability(TypeMirror type) { @@ -172,17 +173,21 @@ protected boolean polyLacksCapability(TypeMirror type) { * *

    When {@code tm} is null, as for an alias written in {@code @DefaultQualifier}, no structural * weakening is applied. + * + *

    A type variable or wildcard is classified by its upper bound, so that, for example, {@code + * >} has the same capabilities as {@code Deque}. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (expandsModifiabilityAliases()) { + TypeMirror bound = tm == null ? null : TypesUtils.upperBound(tm); if (areSameByClass(annotation, Modifiable.class)) { - return tm != null && typeLacksCapability(tm) ? topAnnotation() : positiveCapability(); + return bound != null && typeLacksCapability(bound) ? topAnnotation() : positiveCapability(); } else if (areSameByClass(annotation, Unmodifiable.class)) { - return tm != null && typeLacksCapability(tm) ? topAnnotation() : negativeCapability(); + return bound != null && typeLacksCapability(bound) ? topAnnotation() : negativeCapability(); } else if (areSameByClass(annotation, PolyModifiable.class)) { - return tm != null && polyLacksCapability(tm) ? topAnnotation() : polyCapability(); + return bound != null && polyLacksCapability(bound) ? topAnnotation() : polyCapability(); } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return topAnnotation(); @@ -327,13 +332,14 @@ protected void refineIteratorReturnType( return; } - Tree receiverTree = TreeUtils.getReceiverTree(tree); - if (receiverTree == null) { - // TODO: The receiver is the implicit `this`, whose type is the receiver type of the - // enclosing method. Until that is implemented, leave the declared result alone. + // `getReceiverType()` also handles an implicit `this` receiver, whose type is the receiver + // type of the enclosing method. + AnnotatedTypeMirror receiverType = getReceiverType(tree); + if (receiverType == null) { + // The receiver's type is unknown, so nothing is known about its iterator. + returnType.replaceAnnotation(topAnnotation()); return; } - AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree); // The iterator of a collection that lacks the capability also lacks the capability, even if // the declaration says that the iterator has it (as ArrayList's does). @@ -345,8 +351,9 @@ protected void refineIteratorReturnType( // The receiver has the capability; its iterator does too if the receiver is @IteratorPolyMod. if (receiverType.hasPrimaryAnnotation(positiveCapability())) { AnnotatedTypeMirror iteratorHierarchyType = - getTypeFactoryOfSubchecker(IteratorChecker.class).getAnnotatedType(receiverTree); - if (iteratorHierarchyType.hasPrimaryAnnotation(ITERATOR_POLY_MOD)) { + getTypeFactoryOfSubchecker(IteratorChecker.class).getReceiverType(tree); + if (iteratorHierarchyType != null + && iteratorHierarchyType.hasPrimaryAnnotation(ITERATOR_POLY_MOD)) { returnType.replaceAnnotation(positiveCapability()); return; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index f5ddf28ab69a..9b82c8a1785a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -99,6 +99,9 @@ protected boolean typeLacksCapability(TypeMirror type) { */ @Override protected boolean polyLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index ca68a9c9db12..be9c37aeba46 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -97,6 +97,9 @@ protected boolean typeLacksCapability(TypeMirror type) { */ @Override protected boolean polyLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } diff --git a/checker/tests/modifiability/GenericCapabilityTest.java b/checker/tests/modifiability/GenericCapabilityTest.java new file mode 100644 index 000000000000..d62234abba48 --- /dev/null +++ b/checker/tests/modifiability/GenericCapabilityTest.java @@ -0,0 +1,32 @@ +import java.util.Deque; +import java.util.Map; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; +import org.checkerframework.checker.modifiability.qual.SeqUngrowable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +/** + * A type variable is classified by its upper bound, so an alias written on it expands exactly as it + * does on the bound. + */ +public class GenericCapabilityTest { + + > void modifiableTypeVariable(@Modifiable T deque) { + @SeqGrowable T seqGrowable = deque; + } + + > void unmodifiableTypeVariable(@Unmodifiable T deque) { + @SeqUngrowable T seqUngrowable = deque; + } + + > void entryTypeVariable(@Modifiable T entry) { + // `Map.Entry` has no grow or shrink methods, so `@Modifiable` claims only replacement. + @MaybeGrowable @MaybeShrinkable @Replaceable T canonical = entry; + // :: error: [assignment] + @Growable T growable = entry; + } +} diff --git a/checker/tests/modifiability/ImplicitReceiverIteratorTest.java b/checker/tests/modifiability/ImplicitReceiverIteratorTest.java new file mode 100644 index 000000000000..3c3e3608e7ee --- /dev/null +++ b/checker/tests/modifiability/ImplicitReceiverIteratorTest.java @@ -0,0 +1,38 @@ +import java.util.AbstractCollection; +import java.util.Iterator; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; + +/** + * An iterator method that is called on the implicit {@code this} receiver is refined from the + * receiver type of the enclosing method, just as it would be from an explicit receiver. + */ +public class ImplicitReceiverIteratorTest extends AbstractCollection { + + @Override + public @Shrinkable Iterator iterator() { + throw new Error("not implemented"); + } + + @Override + public int size() { + return 1; + } + + void negativeReceiver(@Unshrinkable ImplicitReceiverIteratorTest this) { + // The iterator of a collection that cannot shrink cannot shrink either, even though the + // declaration says that it can. + @Unshrinkable Iterator it = iterator(); + } + + void unknownReceiver(@MaybeShrinkable ImplicitReceiverIteratorTest this) { + @MaybeShrinkable Iterator it = iterator(); + // :: error: [assignment] + @Shrinkable Iterator shrinkable = iterator(); + } + + void explicitReceiver(@Unshrinkable ImplicitReceiverIteratorTest other) { + @Unshrinkable Iterator it = other.iterator(); + } +} From 374ce625c4320334a5a568834d7bb590e02e6167 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 14 Sep 2026 18:03:14 -0700 Subject: [PATCH 282/308] Add type qualifiers --- .../ModifiabilityBaseAnnotatedTypeFactory.java | 5 ++++- .../checker/modifiability/ModifiabilityBaseVisitor.java | 8 +++++++- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 7dbda75293af..137e03239574 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -6,6 +6,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.modifiability.iterator.IteratorChecker; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; @@ -75,7 +76,9 @@ protected ModifiabilityBaseAnnotatedTypeFactory(BaseTypeChecker checker) { * @param canonicalName the canonical name of a type that is always present * @return the erasure of the named type */ - protected final TypeMirror erasureOf(@FullyQualifiedName String canonicalName) { + protected final TypeMirror erasureOf( + @UnderInitialization(BaseAnnotatedTypeFactory.class) ModifiabilityBaseAnnotatedTypeFactory this, + @FullyQualifiedName String canonicalName) { return types.erasure(elements.getTypeElement(canonicalName).asType()); } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 23b8a9b6884d..796417400cd9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -16,6 +16,7 @@ import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.nullness.qual.Nullable; @@ -97,7 +98,12 @@ public void processClassTree(ClassTree tree) { * @param tree a class */ private void processClassConstructors(ClassTree tree) { - TypeMirror classTM = TreeUtils.elementFromDeclaration(tree).asType(); + TypeElement classElement = TreeUtils.elementFromDeclaration(tree); + if (classElement == null) { + // Some anonymous classes have no element; see TreeUtils.elementFromDeclaration(ClassTree). + return; + } + TypeMirror classTM = classElement.asType(); if (!atypeFactory.isRelevant(classTM)) { return; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 0c265bb3e82e..d9aac183423d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -8,6 +8,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.BottomSeqGrowable; import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; @@ -68,7 +69,9 @@ public SeqGrowAnnotatedTypeFactory(BaseTypeChecker checker) { * @param canonicalName the canonical name of a type that may not be present * @return the erasure of the named type, or null */ - private @Nullable TypeMirror optionalErasureOf(@FullyQualifiedName String canonicalName) { + private @Nullable TypeMirror optionalErasureOf( + @UnderInitialization(ModifiabilityBaseAnnotatedTypeFactory.class) SeqGrowAnnotatedTypeFactory this, + @FullyQualifiedName String canonicalName) { TypeElement element = elements.getTypeElement(canonicalName); return element == null ? null : types.erasure(element.asType()); } From fb304a5adb822c36b2ea39bc453961c5db43fde9 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 11:53:06 -0700 Subject: [PATCH 283/308] Code review --- ...ModifiabilityBaseAnnotatedTypeFactory.java | 30 ++++++------ .../ModifiabilityBaseVisitor.java | 46 ++----------------- .../IteratorAnnotatedTypeFactory.java | 8 ++-- .../iterator/IteratorChecker.java | 1 - 4 files changed, 23 insertions(+), 62 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 137e03239574..1b35694db32d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -125,19 +125,6 @@ protected boolean hasNegativeCapability() { // -- Expansion of the whole-modifiability aliases ---------- - /** - * Returns true if this checker's hierarchy is one of the capabilities that the - * whole-modifiability aliases ({@code @Modifiable}, {@code @Unmodifiable}, - * {@code @MaybeModifiable}, {@code @UnmodifiableParam}, and {@code @PolyModifiable}) expand into. - * The Iterator hierarchy is not: it states what a collection's iterator preserves rather than - * whether a mutating method throws {@link UnsupportedOperationException}. - * - * @return true if the whole-modifiability aliases expand into this checker's hierarchy - */ - protected boolean expandsModifiabilityAliases() { - return true; - } - /** * Returns true if {@code type} structurally cannot support this checker's capability, so that * {@code @Modifiable} and {@code @Unmodifiable} weaken to the top qualifier on {@code type}. For @@ -165,6 +152,19 @@ protected boolean polyLacksCapability(TypeMirror type) { return false; } + /** + * Returns true if this checker's hierarchy is one of the capabilities that the + * whole-modifiability aliases ({@code @Modifiable}, {@code @Unmodifiable}, + * {@code @MaybeModifiable}, {@code @UnmodifiableParam}, and {@code @PolyModifiable}) expand into. + * The Iterator hierarchy is not: it states what a collection's iterator preserves rather than + * whether a mutating method throws {@link UnsupportedOperationException}. + * + * @return true if the whole-modifiability aliases expand into this checker's hierarchy + */ + protected boolean expandsModifiabilityAliases() { + return true; + } + /** * Expands the whole-modifiability aliases into this hierarchy, with structural weakening only for * aliases whose meaning depends on the annotated type. @@ -234,7 +234,7 @@ protected ParameterizedExecutableType methodFromUse( ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { - refinePreservesModifiabilityReturnType(tree, method); + refineReturnTypeForPreservesModifiability(tree, method); } return mType; @@ -260,7 +260,7 @@ protected ParameterizedExecutableType methodFromUse( * @param tree an invocation of a {@code @PreservesModifiability} method * @param methodType the annotated executable type of the invoked method */ - protected void refinePreservesModifiabilityReturnType( + protected void refineReturnTypeForPreservesModifiability( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror returnType = methodType.getReturnType(); if (tree.getArguments().isEmpty() diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 796417400cd9..6173211e908e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -51,10 +51,6 @@ public class ModifiabilityBaseVisitor extends BaseTypeVisitor { - // /** Package that contains the modifiability type-use qualifiers. */ - // private static final String MODIFIABILITY_QUAL_PACKAGE = - // "org.checkerframework.checker.modifiability.qual"; - /** * Create a ModifiabilityBaseVisitor. * @@ -108,10 +104,6 @@ private void processClassConstructors(ClassTree tree) { return; } - // It seems difficult to go from an ExecutableElement to the annotations. - // List constructors = - // ElementFilter.constructorsIn(elem.getEnclosedElements()); - List methods = new ArrayList<>(); List constructors = new ArrayList<>(); for (Tree member : tree.getMembers()) { @@ -225,18 +217,16 @@ private void checkImplOK( // Nothing to check. return; } + // There is no predicate for the bottom annotation. if (!AnnotationUtils.areSameByName(receiverAnno, positiveCapability())) { // The only qualifier left is the bottom one. checker.reportError(method, "bottom.annotation.on.receiver"); return; } - // The receiver requires the capability, so whether the method body should throw - // UnsupportedOperationException depends on the constructor annotation. + // `receiverAnno` is positive; that is, the receiver requires the capability. Whether the + // method body should throw UnsupportedOperationException depends on the constructor annotation. if (!atypeFactory.hasNegativeCapability()) { - // The Iterator hierarchy has no negative qualifier: @IteratorPolyMod states what a - // collection's iterator preserves, not whether a method throws - // UnsupportedOperationException. return; } String constructorAnnoName = @@ -295,7 +285,7 @@ private boolean implIsUOE(MethodTree method) { ExpressionTree identifier = nct.getIdentifier(); if (identifier instanceof IdentifierTree it) { // TODO: This can be fooled if a different UnsupportedOperationException is imported. - // You can check the type of exception: + // A way to prevent that, is to check the type of exception: // types.isSameType(TreeUtils.typeOf(exception), ...); return it.getName().contentEquals("UnsupportedOperationException"); } else if (identifier instanceof MemberSelectTree mst) { @@ -395,34 +385,6 @@ protected boolean shouldCheckReceiverOverrideCapabilityPreservation() { return true; } - // /** - // * Returns true if {@code annotation} is a non-maybe modifiability qualifier that (if written - // on a - // * constructor result type) should trigger a warning about an unverified collection class - // * implementation. - // * - // * @param annotation an annotation mirror written on a constructor result type - // * @return true if {@code annotation} should trigger a warning - // */ - // private boolean isNonTopModifiabilityAnnotation(AnnotationMirror annotation) { - // Element element = annotation.getAnnotationType().asElement(); - // if (!(element instanceof TypeElement typeElement)) { - // return false; - // } - // - // // Only annotations in the modifiability qualifier package are relevant. - // String qualifiedName = typeElement.getQualifiedName().toString(); - // if (!qualifiedName.startsWith(MODIFIABILITY_QUAL_PACKAGE + ".")) { - // return false; - // } - // - // // @Maybe* annotations and @UnmodifiableParam are the top in the hierarchy, so do not warn. - // String simpleName = typeElement.getSimpleName().toString(); - // boolean isTopQualifier = - // simpleName.startsWith("Maybe") || simpleName.equals("UnmodifiableParam"); - // return !isTopQualifier; - // } - // Suppresses the framework's "constructor result must be TOP" check. // Collection constructors (e.g., new ArrayList()) legitimately produce @Modifiable, which is a // subtype of the top type @MaybeModifiable. This suppression is sound: constructors are diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index 080d2014306a..a1b49585f3e8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -56,13 +56,13 @@ protected boolean hasNegativeCapability() { } @Override - protected boolean expandsModifiabilityAliases() { - return false; + protected AnnotationMirror polyCapability() { + return POLY_ITERATOR_POLY_MOD; } @Override - protected AnnotationMirror polyCapability() { - return POLY_ITERATOR_POLY_MOD; + protected boolean expandsModifiabilityAliases() { + return false; } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java index 3e62d399f699..313d5f561fb8 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java @@ -26,7 +26,6 @@ public IteratorChecker() {} @Override protected boolean usesIteratorChecker() { - // This checker would otherwise be its own subchecker. return false; } } From 19e85eeda9309bfb8961cfb7f0f731047a4365ed Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 12:45:19 -0700 Subject: [PATCH 284/308] Check for a bottom receiver independently of the constructors The `bottom.annotation.on.receiver` check was performed within `checkImplOK`, which runs only for a class that has a constructor whose result type is annotated and whose constructors agree with one another, and only for a method that has a body. Writing the bottom qualifier on a receiver is an error regardless, so hoist the check out of `checkImplOK` and perform it on every method of a relevant class. Co-Authored-By: Claude Opus 5 --- .../ModifiabilityBaseVisitor.java | 68 ++++++++++++++----- .../ConstructorResultTypeTest.java | 37 ++++++++++ 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 6173211e908e..f7ed65582912 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -71,7 +71,7 @@ public ModifiabilityBaseVisitor(BaseTypeChecker checker) { * including at the implicit {@code super()} call of a constructor that has no explicit one. * *

    What makes the suppression less unsafe is that the declared modifiability of a class is - * checked against its method bodies; see {@link #processClassConstructors}. That check is not + * checked against its method bodies; see {@link #processClassMembers}. That check is not * complete, so the suppression does permit some unsound code. The check does nothing unless some * constructor of the class declares a qualifier in this hierarchy, and it examines only the * methods that the class declares, not those that it inherits without overriding. @@ -85,15 +85,16 @@ protected void checkThisOrSuperConstructorCall( @Override public void processClassTree(ClassTree tree) { super.processClassTree(tree); - processClassConstructors(tree); + processClassMembers(tree); } /** - * Processes the constructors of a class. + * Processes the members of a class: checks that no method writes the bottom qualifier on its + * receiver, and checks the constructors against one another and against the method bodies. * * @param tree a class */ - private void processClassConstructors(ClassTree tree) { + private void processClassMembers(ClassTree tree) { TypeElement classElement = TreeUtils.elementFromDeclaration(tree); if (classElement == null) { // Some anonymous classes have no element; see TreeUtils.elementFromDeclaration(ClassTree). @@ -116,6 +117,12 @@ private void processClassConstructors(ClassTree tree) { } } + // Writing the bottom qualifier on a receiver is an error no matter what the class's + // constructors say, so this check is done before, and independently of, the checks below. + for (MethodTree method : methods) { + checkReceiverNotBottom(method); + } + boolean thisClassWarned = false; AnnotationMirror constructorAnno = null; @@ -159,6 +166,44 @@ private void processClassConstructors(ClassTree tree) { } } + /** + * Issues an error if {@code method} writes the bottom qualifier on its receiver parameter. + * + *

    The bottom qualifier is a subtype of every qualifier in the hierarchy, so no value has it + * and no method can be called on such a receiver. Writing it is always a mistake, whatever the + * enclosing class's constructors declare, and whether or not the method has a body. + * + * @param method a method declaration + */ + private void checkReceiverNotBottom(MethodTree method) { + if (method.getReceiverParameter() == null) { + // The receiver qualifier was defaulted, so the programmer did not write the bottom + // qualifier on it. + return; + } + AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); + if (receiverType == null) { + return; + } + AnnotationMirror receiverAnno = + receiverType.getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); + if (receiverAnno == null) { + return; + } + // Every modifiability hierarchy contains a top qualifier, a positive qualifier, and a + // polymorphic qualifier. Every hierarchy but the Iterator one also contains a negative and a + // bottom qualifier. There is no predicate for the bottom annotation, so it is recognized by + // eliminating all the others. (In the Iterator hierarchy, the positive qualifier is the + // bottom qualifier, and writing it on a receiver is legitimate.) + if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation()) + || AnnotationUtils.areSameByName(receiverAnno, atypeFactory.polyCapability()) + || AnnotationUtils.areSameByName(receiverAnno, positiveCapability()) + || isNegativeCapability(receiverAnno)) { + return; + } + checker.reportError(method, "bottom.annotation.on.receiver"); + } + /** * Returns the qualifier that states what {@code method} requires of its receiver, or null if the * method states no requirement. @@ -208,19 +253,10 @@ private void processClassConstructors(ClassTree tree) { */ private void checkImplOK( MethodTree method, AnnotationMirror receiverAnno, AnnotationMirror constructorAnno) { - // Every modifiability hierarchy contains a top qualifier, a positive qualifier, and a - // polymorphic qualifier. Every hierarchy but the Iterator one also contains a negative and a - // bottom qualifier. - if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation()) - || AnnotationUtils.areSameByName(receiverAnno, atypeFactory.polyCapability()) - || isNegativeCapability(receiverAnno)) { - // Nothing to check. - return; - } - // There is no predicate for the bottom annotation. if (!AnnotationUtils.areSameByName(receiverAnno, positiveCapability())) { - // The only qualifier left is the bottom one. - checker.reportError(method, "bottom.annotation.on.receiver"); + // The receiver does not require the capability -- it is the top, polymorphic, negative, or + // bottom qualifier -- so the method body is unconstrained. (Writing the bottom qualifier on + // a receiver is diagnosed by checkReceiverNotBottom.) return; } diff --git a/checker/tests/modifiability/ConstructorResultTypeTest.java b/checker/tests/modifiability/ConstructorResultTypeTest.java index 53e245e15c0a..8d5e3abf0716 100644 --- a/checker/tests/modifiability/ConstructorResultTypeTest.java +++ b/checker/tests/modifiability/ConstructorResultTypeTest.java @@ -2,6 +2,7 @@ // which are performed by ModifiabilityBaseVisitor.processClassTree. import java.util.AbstractList; +import java.util.List; import org.checkerframework.checker.modifiability.qual.BottomGrowable; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Ungrowable; @@ -60,4 +61,40 @@ public int size() { // :: error: [bottom.annotation.on.receiver] public void bottomReceiver(@BottomGrowable BottomReceiver this) {} } + + // The interface declares no constructor, so the checks about constructors do nothing, but the + // bottom qualifier on a receiver is still an error. + interface NoConstructor extends List { + // :: error: [bottom.annotation.on.receiver] + default void bottomReceiver(@BottomGrowable NoConstructor this) {} + } + + // The constructors disagree, but the bottom qualifier on a receiver is still an error. + static class InconsistentConstructorsAndBottomReceiver extends AbstractList { + @Growable InconsistentConstructorsAndBottomReceiver() {} + + // :: error: [inconsistent.constructor.result.type] + @Ungrowable InconsistentConstructorsAndBottomReceiver(int capacity) {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + // :: error: [bottom.annotation.on.receiver] + public void bottomReceiver(@BottomGrowable InconsistentConstructorsAndBottomReceiver this) {} + } + + // The method has no body, but the bottom qualifier on its receiver is still an error. + abstract static class AbstractBottomReceiver extends AbstractList { + @Growable AbstractBottomReceiver() {} + + // :: error: [bottom.annotation.on.receiver] + public abstract void bottomReceiver(@BottomGrowable AbstractBottomReceiver this); + } } From 2fcf3f7258ea1c9e9f3492889cf31c7384db2a0a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 12:53:35 -0700 Subject: [PATCH 285/308] Share the modifiability stub files rather than duplicating them `javaparser.astub` was identical in the grow, seqgrow, shrink, and replace packages, as was `ical4j.astub`; an annotation added to one copy would have silently diverged from the other three. Keep one copy of each in the `modifiability` package and load it from `ModifiabilityBaseChecker`. A `@StubFiles` annotation cannot name them, because it is not inherited and its stub files must be in the same directory as the annotated checker class. `getExtraStubFiles()` has neither restriction: its entries are resolved with `Class.getResourceAsStream`, so an absolute resource path works. Co-Authored-By: Claude Opus 5 --- .../ModifiabilityBaseChecker.java | 46 ++++++++++++++++--- .../modifiability/grow/GrowChecker.java | 2 - .../modifiability/{grow => }/ical4j.astub | 0 .../modifiability/{grow => }/javaparser.astub | 0 .../modifiability/replace/ReplaceChecker.java | 2 - .../modifiability/replace/ical4j.astub | 7 --- .../modifiability/replace/javaparser.astub | 13 ------ .../modifiability/seqgrow/SeqGrowChecker.java | 2 - .../modifiability/seqgrow/ical4j.astub | 7 --- .../modifiability/seqgrow/javaparser.astub | 13 ------ .../modifiability/shrink/ShrinkChecker.java | 2 - .../checker/modifiability/shrink/ical4j.astub | 7 --- .../modifiability/shrink/javaparser.astub | 13 ------ 13 files changed, 40 insertions(+), 74 deletions(-) rename checker/src/main/java/org/checkerframework/checker/modifiability/{grow => }/ical4j.astub (100%) rename checker/src/main/java/org/checkerframework/checker/modifiability/{grow => }/javaparser.astub (100%) delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/ical4j.astub delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub delete mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index 150a8c776a02..569fa107492b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -1,5 +1,7 @@ package org.checkerframework.checker.modifiability; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import org.checkerframework.checker.modifiability.iterator.IteratorChecker; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -8,18 +10,50 @@ /** * Base class for the Modifiability sub-checkers. * - *

    This class exists so {@link - * org.checkerframework.framework.source.SourceChecker#getMessagesProperties()} finds the shared - * {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}. The Grow, - * SeqGrow, Shrink, and Replace checkers all report diagnostics whose message keys are defined - * there; without this common superclass, those keys would need to be duplicated in each - * sub-checker's package. + *

    This class exists so that the sub-checkers share two things: + * + *

      + *
    • The {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}, + * which {@link org.checkerframework.framework.source.SourceChecker#getMessagesProperties()} + * finds via this class. The Grow, SeqGrow, Shrink, and Replace checkers all report + * diagnostics whose message keys are defined there; without this common superclass, those + * keys would need to be duplicated in each sub-checker's package. + *
    • The stub files in {@code org.checkerframework.checker.modifiability}; see {@link + * #getExtraStubFiles}. + *
    */ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { + /** + * The stub files that all the Modifiability sub-checkers use, as absolute resource paths. + * + *

    A {@code @}{@link org.checkerframework.framework.qual.StubFiles} annotation cannot be used + * for them, because it is not inherited and its stub files must be in the same directory as the + * checker class that is annotated -- which is what would force one copy of each file per + * sub-checker package. + */ + private static final List SHARED_STUB_FILES = + List.of( + "/org/checkerframework/checker/modifiability/ical4j.astub", + "/org/checkerframework/checker/modifiability/javaparser.astub"); + /** Creates a new ModifiabilityBaseChecker. */ protected ModifiabilityBaseChecker() {} + /** + * {@inheritDoc} + * + *

    This implementation adds the stub files that the sub-checkers share. They are written in + * terms of the capability qualifiers such as {@code @Modifiable}; the Iterator Checker parses + * them too, but ignores every qualifier that is not in its own hierarchy. + */ + @Override + public List getExtraStubFiles() { + List result = new ArrayList<>(super.getExtraStubFiles()); + result.addAll(SHARED_STUB_FILES); + return result; + } + /** * Returns true if this checker refines the result of {@code iterator()} and {@code * listIterator()}, and therefore needs to read the Iterator Checker's qualifiers. The Iterator diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java index 30866eb56249..9eb84bdbd9a3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -5,7 +5,6 @@ import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; -import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -19,7 +18,6 @@ * @checker_framework.manual #modifiability-checker Modifiability Checker */ @RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) -@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"growable", "modifiability"}) public class GrowChecker extends ModifiabilityBaseChecker { /** Creates a GrowChecker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/modifiability/grow/ical4j.astub rename to checker/src/main/java/org/checkerframework/checker/modifiability/ical4j.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/javaparser.astub similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/modifiability/grow/javaparser.astub rename to checker/src/main/java/org/checkerframework/checker/modifiability/javaparser.astub diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java index a9fe28262b34..f91c908d26ec 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -5,7 +5,6 @@ import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; -import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -19,7 +18,6 @@ * @checker_framework.manual #modifiability-checker Modifiability Checker */ @RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) -@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"replaceable", "modifiability"}) public class ReplaceChecker extends ModifiabilityBaseChecker { /** Creates a Replace checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub deleted file mode 100644 index 0791ab0f91d2..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ical4j.astub +++ /dev/null @@ -1,7 +0,0 @@ -import org.checkerframework.checker.modifiability.qual.*; - -package net.fortuna.ical4j.model; - -@Modifiable class ComponentList {} - -@Modifiable class PropertyList {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub deleted file mode 100644 index cc63c87a8309..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/javaparser.astub +++ /dev/null @@ -1,13 +0,0 @@ -import org.checkerframework.checker.modifiability.qual.Modifiable; - -package com.github.javaparser.ast.stmt; - -class BlockStmt { - public @Modifiable NodeList getStatements() {} -} - -package com.github.javaparser.ast; - -class CompilationUnit { - public @Modifiable NodeList getImports() {} -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java index 8d4ec87eef5d..5d64ce4ea6a3 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java @@ -5,7 +5,6 @@ import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; -import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -20,7 +19,6 @@ * @checker_framework.manual #modifiability-checker Modifiability Checker */ @RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) -@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"seqgrowable", "modifiability"}) public class SeqGrowChecker extends ModifiabilityBaseChecker { /** Creates a SeqGrowChecker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/ical4j.astub deleted file mode 100644 index 0791ab0f91d2..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/ical4j.astub +++ /dev/null @@ -1,7 +0,0 @@ -import org.checkerframework.checker.modifiability.qual.*; - -package net.fortuna.ical4j.model; - -@Modifiable class ComponentList {} - -@Modifiable class PropertyList {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub deleted file mode 100644 index cc63c87a8309..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/javaparser.astub +++ /dev/null @@ -1,13 +0,0 @@ -import org.checkerframework.checker.modifiability.qual.Modifiable; - -package com.github.javaparser.ast.stmt; - -class BlockStmt { - public @Modifiable NodeList getStatements() {} -} - -package com.github.javaparser.ast; - -class CompilationUnit { - public @Modifiable NodeList getImports() {} -} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java index 31c48c6e7801..46b799dce40b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -5,7 +5,6 @@ import java.util.Map; import org.checkerframework.checker.modifiability.ModifiabilityBaseChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; -import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -19,7 +18,6 @@ * @checker_framework.manual #modifiability-checker Modifiability Checker */ @RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) -@StubFiles({"ical4j.astub", "javaparser.astub"}) @SuppressWarningsPrefix({"shrinkable", "modifiability"}) public class ShrinkChecker extends ModifiabilityBaseChecker { /** Creates a Shrink checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub deleted file mode 100644 index 0791ab0f91d2..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ical4j.astub +++ /dev/null @@ -1,7 +0,0 @@ -import org.checkerframework.checker.modifiability.qual.*; - -package net.fortuna.ical4j.model; - -@Modifiable class ComponentList {} - -@Modifiable class PropertyList {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub deleted file mode 100644 index cc63c87a8309..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/javaparser.astub +++ /dev/null @@ -1,13 +0,0 @@ -import org.checkerframework.checker.modifiability.qual.Modifiable; - -package com.github.javaparser.ast.stmt; - -class BlockStmt { - public @Modifiable NodeList getStatements() {} -} - -package com.github.javaparser.ast; - -class CompilationUnit { - public @Modifiable NodeList getImports() {} -} From 26ef9d41fd49e6bc2b91567851c5ec567f46885a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 13:42:31 -0700 Subject: [PATCH 286/308] Code review --- .../modifiability/qual/IteratorPolyMod.java | 7 +++- .../qual/MaybeIteratorPolyMod.java | 11 ++++-- .../qual/PreservesModifiability.java | 6 ++- checker/bin/wpi2.sh | 8 ++++ checker/build.gradle | 24 +++--------- ...ModifiabilityBaseAnnotatedTypeFactory.java | 9 ++++- .../ModifiabilityBaseVisitor.java | 7 +++- .../modifiability/ModifiabilityVisitor.java | 38 +++++++++++++++++++ .../checker/modifiability/messages.properties | 1 + .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 12 ++++++ .../PreservesModifiabilityTest.java | 20 +++++++++- .../SeqGrowIntersectionBoundTest.java | 22 +++++++++++ docs/manual/modifiability-checker.tex | 4 +- .../common/basetype/BaseTypeVisitor.java | 11 ++++-- 14 files changed, 144 insertions(+), 36 deletions(-) create mode 100644 checker/tests/modifiability/SeqGrowIntersectionBoundTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java index 65556609bdde..d770f7c97e98 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -16,8 +16,11 @@ * l.listIterator()} also has type {@code @Replaceable}. * *

    If the collection itself is {@code @Unmodifiable}, then its iterator is {@code @Unshrinkable}. - * For any collection whose type is {@code @MaybeIteratorPolyMod}, its iterator is - * {@code @MaybeShrinkable}. + * That holds no matter what the collection's Iterator qualifier is: a {@code @MaybeIteratorPolyMod} + * collection's iterator does not acquire the collection's capabilities, but it does inherit the + * collection's lack of them. Only a collection that has neither the capability nor its negation -- + * for example, a {@code @MaybeShrinkable @MaybeIteratorPolyMod} collection -- has a + * {@code @MaybeShrinkable} iterator. * *

    The Grow, Shrink, and Replace Checkers consult this annotation; the SeqGrow Checker does not, * because an iterator has no sequenced-grow methods. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java index bee5817cbf58..b59bf80c2f7b 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.java @@ -10,9 +10,14 @@ /** * The top qualifier in the Iterator hierarchy. The iterator of the annotated collection is not - * known to preserve the collection's modifiability, so the result of {@code iterator()} and {@code - * listIterator()} is the top qualifier in every capability hierarchy: {@code @MaybeShrinkable}, and - * for a {@code ListIterator} also {@code @MaybeGrowable} and {@code @MaybeReplaceable}. + * known to preserve the collection's capabilities, so the result of {@code iterator()} and {@code + * listIterator()} does not acquire a capability from the collection: it is the top qualifier in + * every capability hierarchy -- {@code @MaybeShrinkable}, and for a {@code ListIterator} also + * {@code @MaybeGrowable} and {@code @MaybeReplaceable}. + * + *

    The iterator does inherit the collection's lack of a capability, since a collection + * that cannot be modified has no iterator that can modify it. For example, an + * {@code @Unshrinkable @MaybeIteratorPolyMod} collection has an {@code @Unshrinkable} iterator. * *

    This is the default qualifier for unannotated types. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java index 224dadcde798..7e35ea32f3bf 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -16,8 +16,10 @@ * return type is also {@code @IteratorPolyMod}. If the first argument has any other qualifier in a * capability hierarchy, then the return type is the top qualifier in that hierarchy. * - *

    If the annotated method has no parameters or returns {@code void}, then this annotation has no - * effect on the method. + *

    It is an error to write this annotation on a method that does not have exactly one formal + * parameter and a non-void result, because the annotation relates the result to the first argument. + * If such a method is nonetheless annotated -- in an annotation file, say, which is not checked -- + * then the annotation has no effect on it. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/bin/wpi2.sh b/checker/bin/wpi2.sh index 24f5cd2f6837..77ee46191341 100755 --- a/checker/bin/wpi2.sh +++ b/checker/bin/wpi2.sh @@ -20,6 +20,14 @@ touch whole-program-inference-output/first-iteration.txt # The `wpi.diff` file is not used; it is produced for diagnostic purposes. while ! diff -ur whole-program-inference-output build/whole-program-inference > wpi.diff; do + # The command did not write any inference output; for example, the build system considered its + # compilation tasks up to date and did not re-run them. Stop rather than proceeding, because + # the rest of the loop body would delete the output of the previous iterations. + if [ ! -d build/whole-program-inference ]; then + echo "wpi2.sh: $* did not create build/whole-program-inference." 1>&2 + echo "wpi2.sh: The output of the previous iterations is in whole-program-inference-output/." 1>&2 + exit 1 + fi rm -rf whole-program-inference-output mv build/whole-program-inference/ whole-program-inference-output mv -f wpi.diff wpi-prev.diff diff --git a/checker/build.gradle b/checker/build.gradle index 483a0c287b1d..832a95c2f90e 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -736,28 +736,14 @@ tasks.register("ainferRelevanceGenerateAjava", Test) { exceptionFormat = "full" events("passed", "skipped", "failed") } - // Resolve these directories at configuration time, and delete them with `File.deleteDir()` - // rather than with `Project.delete()`, because the Gradle configuration cache forbids using - // a project object from within a task action. - // The list includes `build/whole-program-inference`, the directory that WPI writes into. - // `doLast` renames that directory away, but `doLast` does not run when the test fails, so a - // failed run would otherwise leave stale output that this task would fold into its own - // results. `AinferRelevanceAjavaGenerationTest` also reads that directory directly, to - // compare the inference output to the goal files. - List ainferRelevanceDirsToDelete = [ + // `AinferRelevanceAjavaGenerationTest` reads the WPI output directory directly, to compare the + // inference output to the goal files. + List dirsToDelete = [ file("tests/ainfer-relevance/annotated"), - layout.buildDirectory.dir("whole-program-inference").get().asFile, + wpiOutputDirectory(), ] doFirst { - ainferRelevanceDirsToDelete.forEach { dirToDelete -> - // Unlike `Project.delete()`, `File.deleteDir()` reports failure by returning false rather - // than by throwing. (It returns true if the directory does not exist, and false if the - // path exists but is not a directory.) Without this test, this task would run against - // stale output after a failed deletion. - if (!dirToDelete.deleteDir()) { - throw new GradleException("Could not delete ${dirToDelete}") - } - } + DirectoryDeleter.deleteDirectories(dirsToDelete) } doLast { copyNonannotatedToAnnotatedDirectory("ainfer-relevance") diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 1b35694db32d..106969ee86cc 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -243,7 +243,8 @@ protected ParameterizedExecutableType methodFromUse( /** * Refines the return type of a {@code @PreservesModifiability} method. * - *

    If the method has no parameters or returns {@code void}, then the annotation has no effect. + *

    If the method does not have exactly one formal parameter and a non-void result, then the + * annotation has no effect. * *

    Otherwise, if the declared return type has a qualifier other than the top qualifier, that * declared qualifier is used. If the first argument has this checker's positive qualifier (for @@ -263,8 +264,12 @@ protected ParameterizedExecutableType methodFromUse( protected void refineReturnTypeForPreservesModifiability( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror returnType = methodType.getReturnType(); - if (tree.getArguments().isEmpty() + if (methodType.getParameterTypes().size() != 1 + || tree.getArguments().isEmpty() || returnType.getUnderlyingType().getKind() == TypeKind.VOID) { + // The annotation relates the result to the sole argument, so it says nothing about such a + // method. `ModifiabilityVisitor` issues an error for a source declaration like this; the + // declaration might also come from an annotation file, which is not checked. return; } AnnotationMirror declaredReturnAnno = diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index f7ed65582912..54e8f5d42994 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -397,11 +397,14 @@ protected boolean checkOverride( if (overriddenReceiver.hasPrimaryAnnotation(positiveCapability) && !overriderReceiver.hasPrimaryAnnotation(positiveCapability)) { + // Use FoundRequired, as the framework's own `override.receiver` report does, so that the + // message key is rendered the same way no matter which check produced it. + FoundRequired pair = FoundRequired.of(overriderReceiver, overriddenReceiver); checker.reportError( overriderTree, "override.receiver", - overriderReceiver, - overriddenReceiver, + pair.found, + pair.required, overriderType, overriderMethodType, overriddenType, diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index c48494eea3a2..eb36b4e4a0df 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -3,6 +3,9 @@ import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.MethodTree; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.framework.source.SourceVisitor; import org.checkerframework.javacutil.AnnotationUtils; @@ -14,6 +17,10 @@ public class ModifiabilityVisitor extends SourceVisitor { /** Fully-qualified name for {@link UnmodifiableParam}. */ private static final String unmodifiableParamQualifiedName = UnmodifiableParam.class.getName(); + /** Fully-qualified name for {@link PreservesModifiability}. */ + private static final String preservesModifiabilityQualifiedName = + PreservesModifiability.class.getName(); + /** {@link ModifiabilityChecker}. */ private final ModifiabilityChecker checker; @@ -41,6 +48,7 @@ protected ModifiabilityVisitor(ModifiabilityChecker checker) { @Override public Void visitMethod(MethodTree tree, Void p) { storeSuppressWarningsAnno(tree); + checkPreservesModifiabilityLocation(tree); scan(tree.getModifiers(), p); scan(tree.getReturnType(), p); scan(tree.getTypeParameters(), p); @@ -54,6 +62,36 @@ public Void visitMethod(MethodTree tree, Void p) { return null; } + /** + * Issues an error if {@code tree} is annotated as {@code @PreservesModifiability} but is not a + * method that has exactly one formal parameter and a non-void result. + * + *

    The annotation relates the method's result to its first argument, so it says nothing about + * such a method. Worse, on a method with more than one formal parameter it would silently use the + * first argument, which need not be the one that the programmer had in mind. + * + * @param tree a method declaration + */ + private void checkPreservesModifiabilityLocation(MethodTree tree) { + ExecutableElement methodElt = TreeUtils.elementFromDeclaration(tree); + if (methodElt == null || methodElt.getAnnotation(PreservesModifiability.class) == null) { + return; + } + if (methodElt.getParameters().size() == 1 + && methodElt.getReturnType().getKind() != TypeKind.VOID) { + return; + } + for (AnnotationTree annoTree : tree.getModifiers().getAnnotations()) { + AnnotationMirror anno = TreeUtils.annotationFromAnnotationTree(annoTree); + if (anno != null + && AnnotationUtils.areSameByName(anno, preservesModifiabilityQualifiedName)) { + checker.reportError(annoTree, "preservesmodifiability.location"); + return; + } + } + checker.reportError(tree, "preservesmodifiability.location"); + } + @Override public Void visitAnnotation(AnnotationTree tree, Void p) { // The implementation of annotationFromAnnotationTree just returns a field of tree, so it's fine diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties index 3b4e585ad56a..95066069c79a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -1,4 +1,5 @@ unmodparam.location=@UnmodifiableParam may only be written within the type of a method or constructor formal parameter, including the receiver parameter +preservesmodifiability.location=@PreservesModifiability may only be written on a method declaration that has exactly one formal parameter and a non-void result inconsistent.constructor.result.type=Constructor result type %s is inconsistent with a previous constructor with result type %s bottom.annotation.on.receiver=Do not write the bottom annotation on a receiver method.implementation.is.uoe=Method implementation should not be `throw new UnsupportedOperationException()`, because the constructors of this class are %s diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index d9aac183423d..f0921c731eb5 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -6,6 +6,7 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.IntersectionType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.initialization.qual.UnderInitialization; @@ -117,6 +118,17 @@ protected Set> createSupportedTypeQualifiers() { */ @Override protected boolean typeLacksCapability(TypeMirror type) { + if (type.getKind() == TypeKind.INTERSECTION) { + // A value of an intersection type is a value of each of its bounds, so it has the capability + // if any bound does. For example, the upper bound of ` & Cloneable>` + // is an intersection type, and such a `T` can be sequenced-grown. + for (TypeMirror bound : ((IntersectionType) type).getBounds()) { + if (!typeLacksCapability(bound)) { + return false; + } + } + return true; + } if (type.getKind() != TypeKind.DECLARED) { return true; } diff --git a/checker/tests/modifiability/PreservesModifiabilityTest.java b/checker/tests/modifiability/PreservesModifiabilityTest.java index 4e1c6ecf770d..7ac88d238a87 100644 --- a/checker/tests/modifiability/PreservesModifiabilityTest.java +++ b/checker/tests/modifiability/PreservesModifiabilityTest.java @@ -19,9 +19,24 @@ static List unannotated(Collection values) { return new ArrayList<>(values); } + // The annotation relates the result to the first argument, so it may not be written on a method + // that returns void or that does not have exactly one formal parameter. + // :: error: [preservesmodifiability.location] @PreservesModifiability static void annotatedVoid(Collection values) {} + // :: error: [preservesmodifiability.location] + @PreservesModifiability + static List annotatedNoArguments() { + return new ArrayList<>(); + } + + // :: error: [preservesmodifiability.location] + @PreservesModifiability + static List annotatedTwoArguments(Collection values, Collection other) { + return new ArrayList<>(values); + } + void preservesCapabilities( @Growable List growable, @Shrinkable List shrinkable, @@ -53,7 +68,10 @@ void unannotatedDoesNotPreserve( @Modifiable List m = unannotated(modifiable); } - void voidAnnotatedMethodHasNoEffect(@Growable List growable) { + // Despite the error on its declaration, the annotation has no effect on a call to such a method. + void misplacedAnnotationHasNoEffect(@Growable List growable) { annotatedVoid(growable); + // :: error: [assignment] + @Growable List g = annotatedTwoArguments(growable, growable); } } diff --git a/checker/tests/modifiability/SeqGrowIntersectionBoundTest.java b/checker/tests/modifiability/SeqGrowIntersectionBoundTest.java new file mode 100644 index 000000000000..35437fbb3b17 --- /dev/null +++ b/checker/tests/modifiability/SeqGrowIntersectionBoundTest.java @@ -0,0 +1,22 @@ +// Tests the expansion of `@Modifiable` on a type variable whose upper bound is an intersection +// type. A value of an intersection type is a value of each of its bounds, so it has a capability +// if any bound does. + +import java.util.Deque; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; + +public class SeqGrowIntersectionBoundTest { + + // `Deque` has sequenced-grow methods, so `@Modifiable` expands to `@SeqGrowable`. + & Cloneable> void intersectionBoundHasCapability(@Modifiable T deque) { + @SeqGrowable T s = deque; + } + + // No bound has sequenced-grow methods, so `@Modifiable` expands to `@MaybeSeqGrowable`. + & Cloneable> void intersectionBoundLacksCapability(@Modifiable T set) { + @MaybeSeqGrowable T m = set; + } +} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index bcfe11c9975e..58d188f2cdd0 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -455,7 +455,7 @@ \<@MaybeSeqGrowable>, \<@MaybeShrinkable>, \<@MaybeReplaceable>, or \<@MaybeIteratorPolyMod>) in the corresponding hierarchy. -This annotation should only be written on non-void, one-argument method declarations. +This annotation may only be written on non-void, one-argument method declarations. \end{description} @@ -508,7 +508,7 @@ The following rules define this weakening. The first qualifier in each capability row is used for \<@Modifiable>; the second is used for \<@Unmodifiable>. If the weakening condition holds, both aliases use the -corresponding \<@Maybe*> qualifier instead.x +corresponding \<@Maybe*> qualifier instead. 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 d34a783dec2d..99d5c5878ed5 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -3526,8 +3526,12 @@ private String fileAndLineNumber(Tree tree) { /** * Class that creates string representations of {@link AnnotatedTypeMirror}s which are only * verbose if required to differentiate the two types. + * + *

    It is protected so that a subclass that reports one of the message keys that take a + * found/required pair, such as {@code override.receiver}, renders the pair the same way that this + * class does. */ - private static final class FoundRequired { + protected static final class FoundRequired { /** The found type. */ public final String found; @@ -3559,7 +3563,7 @@ private FoundRequired(AnnotatedTypeMirror found, AnnotatedTypeParameterBounds re * Creates string representations of {@link AnnotatedTypeMirror}s which are only verbose if * required to differentiate the two types. */ - static FoundRequired of(AnnotatedTypeMirror found, AnnotatedTypeMirror required) { + public static FoundRequired of(AnnotatedTypeMirror found, AnnotatedTypeMirror required) { return new FoundRequired(found, required); } @@ -3568,7 +3572,8 @@ static FoundRequired of(AnnotatedTypeMirror found, AnnotatedTypeMirror required) * AnnotatedTypeParameterBounds}s which are only verbose if required to differentiate the two * types. */ - static FoundRequired of(AnnotatedTypeMirror found, AnnotatedTypeParameterBounds required) { + public static FoundRequired of( + AnnotatedTypeMirror found, AnnotatedTypeParameterBounds required) { return new FoundRequired(found, required); } } From 4bedf8fccd0b5f769dbfb0205709a30d3972d8b5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 14:27:06 -0700 Subject: [PATCH 287/308] Address code review findings * `VivifyingMap.prune()` requires a `@Shrinkable` receiver, and the fields that are pruned are annotated as `@Shrinkable`. Previously `prune()` removed entries through an iterator that a `@SuppressWarnings` forced to be shrinkable, so it made no requirement of its receiver at all. * `Issue8053` again invokes `computeIfAbsent` on a field, as Beam does; the field is annotated `@Modifiable` rather than being rewritten as a local variable, whose type is refined from its initializer and so no longer reproduces the inference context of the bug. * `@Modifiable`, `@Unmodifiable`, and `@PolyModifiable` expand correctly on a type variable whose upper bound is an intersection type, in every sub-checker rather than only in SeqGrow. A value of an intersection type has a capability if any bound does, so a bound lacks a capability only if every one of its bounds does. * `@PreservesModifiability` is not permitted on a varargs method, and has no effect on a call to one. Such a method has a single formal parameter, but the first argument of a call to it is an element of the varargs array rather than that parameter. Co-Authored-By: Claude Opus 5 --- .../afu/scenelib/el/ABlock.java | 3 +- .../afu/scenelib/el/AClass.java | 15 +++--- .../afu/scenelib/el/AExpression.java | 13 ++--- .../afu/scenelib/el/AMethod.java | 14 +++-- .../afu/scenelib/el/ATypeElement.java | 4 +- .../afu/scenelib/util/coll/VivifyingMap.java | 8 ++- .../qual/PreservesModifiability.java | 8 +-- ...ModifiabilityBaseAnnotatedTypeFactory.java | 54 +++++++++++++++---- .../modifiability/ModifiabilityVisitor.java | 9 +++- .../checker/modifiability/messages.properties | 2 +- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 12 ----- ...ndTest.java => IntersectionBoundTest.java} | 20 ++++++- .../PreservesModifiabilityTest.java | 11 ++++ docs/manual/modifiability-checker.tex | 5 +- framework/tests/all-systems/Issue8053.java | 13 ++--- 15 files changed, 135 insertions(+), 56 deletions(-) rename checker/tests/modifiability/{SeqGrowIntersectionBoundTest.java => IntersectionBoundTest.java} (54%) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ABlock.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ABlock.java index 068db720b0a1..b58d3270d4a0 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ABlock.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ABlock.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Objects; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * ABlock has local variables in scope. We currently directly use them only for static initializer @@ -12,7 +13,7 @@ public class ABlock extends AExpression { // Currently we don't validate the local locations (e.g., that no two // distinct ranges for the same index overlap). /** The method's annotated local variables; map key contains local variable location numbers. */ - public final VivifyingMap locals = + public final @Shrinkable VivifyingMap locals = AField.newVivifyingLHMap_AF(); /** diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java index f2c0947c5aec..ea2b47b9023e 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AClass.java @@ -16,16 +16,17 @@ import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.plumelib.util.MapsP; /** An annotated class. */ public class AClass extends ADeclaration { /** The class's annotated type parameter bounds. */ - public final @Growable @Replaceable VivifyingMap bounds = + public final @Growable @Replaceable @Shrinkable VivifyingMap bounds = ATypeElement.newVivifyingLHMap_ATE(); /** -1 maps to superclass, non-negative integers map to implemented or extended interfaces. */ - public final VivifyingMap extendsImplements = + public final @Shrinkable VivifyingMap extendsImplements = ATypeElement.newVivifyingLHMap_ATE(); /** @@ -34,16 +35,18 @@ public class AClass extends ADeclaration { * The annotation scene library does not validate the keys, nor does it check that annotated * subelements of the {@link AMethod}s exist in the signature. */ - public final @Growable VivifyingMap methods = createMethodMap(); + public final @Growable @Shrinkable VivifyingMap methods = createMethodMap(); /** The class's annotated static initializer blocks; map key is the index of the block. */ - public final @Growable VivifyingMap staticInits = createInitBlockMap(); + public final @Growable @Shrinkable VivifyingMap staticInits = + createInitBlockMap(); /** The class's annotated instance initializer blocks; map key is the index of the block. */ - public final @Growable VivifyingMap instanceInits = createInitBlockMap(); + public final @Growable @Shrinkable VivifyingMap instanceInits = + createInitBlockMap(); /** The class's annotated fields; map key is field name. */ - public final @Growable VivifyingMap fields = + public final @Growable @Shrinkable VivifyingMap fields = AField.newVivifyingLHMap_AF(); /** The class's annotated field initializers; map key is field name. */ diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java index 8aff47b820ac..9aa52d6ba1f3 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java @@ -7,6 +7,7 @@ import java.util.SortedMap; import java.util.TreeMap; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * Manages all annotations within expressions, that is, annotations on typecasts, instanceofs, and @@ -14,37 +15,37 @@ */ public class AExpression extends AElement { /** The method's annotated typecasts; map key is the offset of the checkcast bytecode. */ - public final VivifyingMap typecasts = + public final @Shrinkable VivifyingMap typecasts = ATypeElement.newVivifyingLHMap_ATE(); /** * The method's annotated "instanceof" tests; map key is the offset of the instanceof bytecode. */ - public final VivifyingMap instanceofs = + public final @Shrinkable VivifyingMap instanceofs = ATypeElement.newVivifyingLHMap_ATE(); /** The method's annotated "new" invocations; map key is the offset of the new bytecode. */ - public final VivifyingMap news = + public final @Shrinkable VivifyingMap news = ATypeElement.newVivifyingLHMap_ATE(); /** * A method invocation's annotated type arguments; map key is the offset of the invokestatic * bytecode */ - public final VivifyingMap calls = + public final @Shrinkable VivifyingMap calls = ATypeElement.newVivifyingLHMap_ATE(); /** * A member reference's annotated type parameters; map key is the offset of the invokestatic * bytecode */ - public final VivifyingMap refs = + public final @Shrinkable VivifyingMap refs = ATypeElement.newVivifyingLHMap_ATE(); /** * The method's annotated lambda expressions; map key is the offset of the invokedynamic bytecode */ - public final VivifyingMap funs = + public final @Shrinkable VivifyingMap funs = new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AMethod createValueFor(RelativeLocation k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AMethod.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AMethod.java index f7106f38b924..798ec19d1d5e 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AMethod.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AMethod.java @@ -14,6 +14,7 @@ import org.checkerframework.afu.scenelib.Annotation; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.nullness.qual.Nullable; /** An annotated method; contains bounds, return, parameters, receiver, and throws. */ @@ -28,7 +29,7 @@ public class AMethod extends ADeclaration { private /*@Nullable*/ List typeParameters = null; /** The method's annotated type parameter bounds. */ - public final VivifyingMap bounds = + public final @Shrinkable VivifyingMap bounds = ATypeElement.newVivifyingLHMap_ATE(); /** The return type of the method, or null if the method's return type is unknown or void. */ @@ -41,10 +42,11 @@ public class AMethod extends ADeclaration { public final AField receiver; // initialized in constructor /** The method's annotated parameters; map key is parameter index, starting at 0. */ - public final VivifyingMap parameters = AField.newVivifyingLHMap_AF(); + public final @Shrinkable VivifyingMap parameters = + AField.newVivifyingLHMap_AF(); /** Exceptions that are thrown. */ - public final VivifyingMap throwsException = + public final @Shrinkable VivifyingMap throwsException = ATypeElement.newVivifyingLHMap_ATE(); /** @@ -52,14 +54,16 @@ public class AMethod extends ADeclaration { * expression. */ // TODO: The map value type should probably be ATypeElement instead. - public final VivifyingMap preconditions = AField.newVivifyingLHMap_AF(); + public final @Shrinkable VivifyingMap preconditions = + AField.newVivifyingLHMap_AF(); /** * Types of expressions at exit from the method. The map key is the string representation of the * expression. */ // TODO: The map value type should probably be ATypeElement instead. - public final VivifyingMap postconditions = AField.newVivifyingLHMap_AF(); + public final @Shrinkable VivifyingMap postconditions = + AField.newVivifyingLHMap_AF(); /** * Clients set this before printing the AMethod. diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java index 5ec7de669626..c38e5d7bda7c 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java @@ -7,6 +7,7 @@ import org.checkerframework.afu.scenelib.Annotation; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * An {@link AElement} that represents a type might have annotations on inner types ("generic/array" @@ -23,7 +24,8 @@ public class ATypeElement extends AElement { /** The annotated inner types; map key is the inner type location. */ - public final VivifyingMap, ATypeElement> innerTypes = newVivifyingLHMap_ATE(); + public final @Shrinkable VivifyingMap, ATypeElement> innerTypes = + newVivifyingLHMap_ATE(); /** * Construct a new ATypeElement from its description. diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index ac6a234c089b..b6f18f716236 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -52,9 +52,13 @@ public V getVivify(K k) { */ protected abstract V createValueFor(K k); - /** Prunes this map by deleting entries with empty values. */ + /** + * Prunes this map by deleting entries with empty values. + * + *

    The receiver must be {@code @Shrinkable}, because pruning removes entries from the map. + */ @SuppressWarnings("modifiability:assignment") // entrySet() is not @IteratorPolyMod. - public void prune() { + public void prune(@Shrinkable VivifyingMap this) { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { // but using an iterator affords efficient deletion. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java index 7e35ea32f3bf..6bb2d47df6bd 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -17,9 +17,11 @@ * capability hierarchy, then the return type is the top qualifier in that hierarchy. * *

    It is an error to write this annotation on a method that does not have exactly one formal - * parameter and a non-void result, because the annotation relates the result to the first argument. - * If such a method is nonetheless annotated -- in an annotation file, say, which is not checked -- - * then the annotation has no effect on it. + * parameter, which is not a varargs parameter, and a non-void result, because the annotation + * relates the result to the first argument. (In a call to a varargs method, the first argument is + * an element of the varargs array rather than the sole formal parameter.) If such a method is + * nonetheless annotated -- in an annotation file, say, which is not checked -- then the annotation + * has no effect on it. * * @checker_framework.manual #modifiability-checker Modifiability Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 106969ee86cc..c60fd510bf2a 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -2,8 +2,10 @@ import com.sun.source.tree.MethodInvocationTree; import java.util.List; +import java.util.function.Predicate; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.IntersectionType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.initialization.qual.UnderInitialization; @@ -131,7 +133,8 @@ protected boolean hasNegativeCapability() { * example, {@code Map.Entry} cannot grow. * * @param type the type on which an alias was written; it is an upper bound, so it is never a type - * variable or a wildcard + * variable or a wildcard, and {@link #lacksCapability} has already decomposed intersection + * types, so it is never an intersection type either * @return true if {@code type} structurally cannot support this checker's capability */ protected boolean typeLacksCapability(TypeMirror type) { @@ -145,13 +148,37 @@ protected boolean typeLacksCapability(TypeMirror type) { * exercise; for example, {@code Map.Entry} carries the replace capability of its map. * * @param type the type on which {@code @PolyModifiable} was written; it is an upper bound, so it - * is never a type variable or a wildcard + * is never a type variable or a wildcard, and {@link #lacksCapability} has already decomposed + * intersection types, so it is never an intersection type either * @return true if {@code @PolyModifiable} weakens to the top qualifier on {@code type} */ protected boolean polyLacksCapability(TypeMirror type) { return false; } + /** + * Returns the result of {@code lacks} on {@code type}, decomposing an intersection type. A value + * of an intersection type is a value of each of its bounds, so it has a capability if any bound + * does; for example, the upper bound of {@code & Cloneable>} is an + * intersection type, and such a {@code T} can be sequenced-grown. + * + * @param type the type on which an alias was written; it is an upper bound, so it is never a type + * variable or a wildcard + * @param lacks {@link #typeLacksCapability} or {@link #polyLacksCapability} + * @return true if {@code lacks} holds of {@code type} or of every bound of {@code type} + */ + private boolean lacksCapability(TypeMirror type, Predicate lacks) { + if (type.getKind() == TypeKind.INTERSECTION) { + for (TypeMirror bound : ((IntersectionType) type).getBounds()) { + if (!lacksCapability(bound, lacks)) { + return false; + } + } + return true; + } + return lacks.test(type); + } + /** * Returns true if this checker's hierarchy is one of the capabilities that the * whole-modifiability aliases ({@code @Modifiable}, {@code @Unmodifiable}, @@ -186,11 +213,17 @@ public AnnotationMirror canonicalAnnotation( if (expandsModifiabilityAliases()) { TypeMirror bound = tm == null ? null : TypesUtils.upperBound(tm); if (areSameByClass(annotation, Modifiable.class)) { - return bound != null && typeLacksCapability(bound) ? topAnnotation() : positiveCapability(); + return bound != null && lacksCapability(bound, this::typeLacksCapability) + ? topAnnotation() + : positiveCapability(); } else if (areSameByClass(annotation, Unmodifiable.class)) { - return bound != null && typeLacksCapability(bound) ? topAnnotation() : negativeCapability(); + return bound != null && lacksCapability(bound, this::typeLacksCapability) + ? topAnnotation() + : negativeCapability(); } else if (areSameByClass(annotation, PolyModifiable.class)) { - return bound != null && polyLacksCapability(bound) ? topAnnotation() : polyCapability(); + return bound != null && lacksCapability(bound, this::polyLacksCapability) + ? topAnnotation() + : polyCapability(); } else if (areSameByClass(annotation, MaybeModifiable.class) || areSameByClass(annotation, UnmodifiableParam.class)) { return topAnnotation(); @@ -243,8 +276,8 @@ protected ParameterizedExecutableType methodFromUse( /** * Refines the return type of a {@code @PreservesModifiability} method. * - *

    If the method does not have exactly one formal parameter and a non-void result, then the - * annotation has no effect. + *

    If the method does not have exactly one formal parameter, which is not a varargs parameter, + * and a non-void result, then the annotation has no effect. * *

    Otherwise, if the declared return type has a qualifier other than the top qualifier, that * declared qualifier is used. If the first argument has this checker's positive qualifier (for @@ -265,11 +298,14 @@ protected void refineReturnTypeForPreservesModifiability( MethodInvocationTree tree, AnnotatedExecutableType methodType) { AnnotatedTypeMirror returnType = methodType.getReturnType(); if (methodType.getParameterTypes().size() != 1 + || methodType.getElement().isVarArgs() || tree.getArguments().isEmpty() || returnType.getUnderlyingType().getKind() == TypeKind.VOID) { // The annotation relates the result to the sole argument, so it says nothing about such a - // method. `ModifiabilityVisitor` issues an error for a source declaration like this; the - // declaration might also come from an annotation file, which is not checked. + // method. (For a varargs method, the first argument of a call is an element of the varargs + // array rather than the sole formal parameter.) `ModifiabilityVisitor` issues an error for a + // source declaration like this; the declaration might also come from an annotation file, + // which is not checked. return; } AnnotationMirror declaredReturnAnno = diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index eb36b4e4a0df..13f63ac66b2d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -64,11 +64,15 @@ public Void visitMethod(MethodTree tree, Void p) { /** * Issues an error if {@code tree} is annotated as {@code @PreservesModifiability} but is not a - * method that has exactly one formal parameter and a non-void result. + * method that has exactly one formal parameter, which is not a varargs parameter, and a non-void + * result. * *

    The annotation relates the method's result to its first argument, so it says nothing about * such a method. Worse, on a method with more than one formal parameter it would silently use the - * first argument, which need not be the one that the programmer had in mind. + * first argument, which need not be the one that the programmer had in mind. A varargs method is + * rejected for the same reason: a call's first argument is an element of the varargs array rather + * than the sole formal parameter, so the annotation would relate the result to a value of a + * different type. * * @param tree a method declaration */ @@ -78,6 +82,7 @@ private void checkPreservesModifiabilityLocation(MethodTree tree) { return; } if (methodElt.getParameters().size() == 1 + && !methodElt.isVarArgs() && methodElt.getReturnType().getKind() != TypeKind.VOID) { return; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties index 95066069c79a..98b927329486 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -1,5 +1,5 @@ unmodparam.location=@UnmodifiableParam may only be written within the type of a method or constructor formal parameter, including the receiver parameter -preservesmodifiability.location=@PreservesModifiability may only be written on a method declaration that has exactly one formal parameter and a non-void result +preservesmodifiability.location=@PreservesModifiability may only be written on a method declaration that has exactly one formal parameter, which is not a varargs parameter, and a non-void result inconsistent.constructor.result.type=Constructor result type %s is inconsistent with a previous constructor with result type %s bottom.annotation.on.receiver=Do not write the bottom annotation on a receiver method.implementation.is.uoe=Method implementation should not be `throw new UnsupportedOperationException()`, because the constructors of this class are %s diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index f0921c731eb5..d9aac183423d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -6,7 +6,6 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.IntersectionType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.initialization.qual.UnderInitialization; @@ -118,17 +117,6 @@ protected Set> createSupportedTypeQualifiers() { */ @Override protected boolean typeLacksCapability(TypeMirror type) { - if (type.getKind() == TypeKind.INTERSECTION) { - // A value of an intersection type is a value of each of its bounds, so it has the capability - // if any bound does. For example, the upper bound of ` & Cloneable>` - // is an intersection type, and such a `T` can be sequenced-grown. - for (TypeMirror bound : ((IntersectionType) type).getBounds()) { - if (!typeLacksCapability(bound)) { - return false; - } - } - return true; - } if (type.getKind() != TypeKind.DECLARED) { return true; } diff --git a/checker/tests/modifiability/SeqGrowIntersectionBoundTest.java b/checker/tests/modifiability/IntersectionBoundTest.java similarity index 54% rename from checker/tests/modifiability/SeqGrowIntersectionBoundTest.java rename to checker/tests/modifiability/IntersectionBoundTest.java index 35437fbb3b17..0cb8647b75ae 100644 --- a/checker/tests/modifiability/SeqGrowIntersectionBoundTest.java +++ b/checker/tests/modifiability/IntersectionBoundTest.java @@ -3,12 +3,16 @@ // if any bound does. import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.SeqGrowable; -public class SeqGrowIntersectionBoundTest { +public class IntersectionBoundTest { // `Deque` has sequenced-grow methods, so `@Modifiable` expands to `@SeqGrowable`. & Cloneable> void intersectionBoundHasCapability(@Modifiable T deque) { @@ -19,4 +23,18 @@ & Cloneable> void intersectionBoundHasCapability(@Modif & Cloneable> void intersectionBoundLacksCapability(@Modifiable T set) { @MaybeSeqGrowable T m = set; } + + // `List` has grow methods, so `@Modifiable` expands to `@Growable`, even though the other bound + // is not a collection at all. + & Cloneable> void growIntersectionBoundHasCapability(@Modifiable T list) { + @Growable T g = list; + } + + // Neither `Map.Entry` nor a plain `Iterator` has grow methods, so `@Modifiable` expands to + // `@MaybeGrowable`, which does not satisfy `@Growable`. + & Iterator> + void growIntersectionBoundLacksCapability(@Modifiable T entry) { + // :: error: [assignment] + @Growable T g = entry; + } } diff --git a/checker/tests/modifiability/PreservesModifiabilityTest.java b/checker/tests/modifiability/PreservesModifiabilityTest.java index 7ac88d238a87..33bbad2bb9cb 100644 --- a/checker/tests/modifiability/PreservesModifiabilityTest.java +++ b/checker/tests/modifiability/PreservesModifiabilityTest.java @@ -37,6 +37,15 @@ static List annotatedTwoArguments(Collection values, Collection oth return new ArrayList<>(values); } + // A varargs method has one formal parameter, but the first argument of a call to it is an + // element of the varargs array rather than that parameter. + // :: error: [preservesmodifiability.location] + @PreservesModifiability + @SafeVarargs + static List annotatedVarargs(Collection... values) { + return new ArrayList<>(values[0]); + } + void preservesCapabilities( @Growable List growable, @Shrinkable List shrinkable, @@ -73,5 +82,7 @@ void misplacedAnnotationHasNoEffect(@Growable List growable) { annotatedVoid(growable); // :: error: [assignment] @Growable List g = annotatedTwoArguments(growable, growable); + // :: error: [assignment] + @Growable List v = annotatedVarargs(growable, growable); } } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 58d188f2cdd0..ae6ca970dc5b 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -455,7 +455,10 @@ \<@MaybeSeqGrowable>, \<@MaybeShrinkable>, \<@MaybeReplaceable>, or \<@MaybeIteratorPolyMod>) in the corresponding hierarchy. -This annotation may only be written on non-void, one-argument method declarations. +This annotation may only be written on non-void, one-argument method +declarations. The formal parameter may not be a varargs parameter, because +the first argument of a call to a varargs method is an element of the varargs +array rather than the sole formal parameter. \end{description} diff --git a/framework/tests/all-systems/Issue8053.java b/framework/tests/all-systems/Issue8053.java index 581f9042e63b..84ed368ba536 100644 --- a/framework/tests/all-systems/Issue8053.java +++ b/framework/tests/all-systems/Issue8053.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.checkerframework.checker.modifiability.qual.Modifiable; /** * A generic method whose return type contains a wildcard, invoked as the body of a lambda passed to @@ -41,16 +42,16 @@ interface Getter {} return types.stream().map(t -> wild(t)).collect(Collectors.toList()); } + static final @Modifiable Map>> CACHE = new HashMap<>(); + static native

    Getter createGetter(Class

    clazz, String name); // The Beam shape: the whole thing is the mapping function of Map.computeIfAbsent, and the - // Class argument is itself wildcard-typed. The map is a local variable rather than a field so - // that a checker that reasons about the receiver of `computeIfAbsent` (such as the Modifiability - // Checker, whose `computeIfAbsent` requires a receiver that permits growing) can refine its type - // from the `new HashMap<>()`; a field's type is not refined that way. + // Class argument is itself wildcard-typed. The receiver must be a field, as in Beam: a field's + // type is not refined from its initializer, so the inference context differs from that of a + // local variable. static List> beamShape(Class clazz, List types, String key) { - Map>> cache = new HashMap<>(); - return cache.computeIfAbsent( + return CACHE.computeIfAbsent( key, c -> types.stream().map(t -> createGetter(clazz, t)).collect(Collectors.toList())); } From 4cdb4f184f17b6579cecfe4fb93cfc77d93bd7ef Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 14:59:46 -0700 Subject: [PATCH 288/308] Move the wpi2.sh changes to the wpi2 branch Co-Authored-By: Claude Opus 5 --- checker/bin/wpi2.sh | 37 ---------------------- docs/CHANGELOG.md | 2 -- docs/manual/inference.tex | 65 ++------------------------------------- 3 files changed, 2 insertions(+), 102 deletions(-) delete mode 100755 checker/bin/wpi2.sh diff --git a/checker/bin/wpi2.sh b/checker/bin/wpi2.sh deleted file mode 100755 index 77ee46191341..000000000000 --- a/checker/bin/wpi2.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh - -# This script performs whole-program inference on a project. -# Run it from the project root. -# Its output is a directory `whole-program-inference-output/` -# that contains .ajava files. - -# For usage, see the "Whole-program inference" -# section of the Checker Framework manual: -# https://checkerframework.org/manual/#whole-program-inference - -# Exit the script if any statement fails. -set -e - -rm -rf build/whole-program-inference -mkdir -p build/whole-program-inference -rm -rf whole-program-inference-output -mkdir -p whole-program-inference-output -touch whole-program-inference-output/first-iteration.txt - -# The `wpi.diff` file is not used; it is produced for diagnostic purposes. -while ! diff -ur whole-program-inference-output build/whole-program-inference > wpi.diff; do - # The command did not write any inference output; for example, the build system considered its - # compilation tasks up to date and did not re-run them. Stop rather than proceeding, because - # the rest of the loop body would delete the output of the previous iterations. - if [ ! -d build/whole-program-inference ]; then - echo "wpi2.sh: $* did not create build/whole-program-inference." 1>&2 - echo "wpi2.sh: The output of the previous iterations is in whole-program-inference-output/." 1>&2 - exit 1 - fi - rm -rf whole-program-inference-output - mv build/whole-program-inference/ whole-program-inference-output - mv -f wpi.diff wpi-prev.diff - "$@" -done - -rm -rf wpi.diff wpi-prev.diff diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index db466a598a92..72cea8a82e60 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -82,8 +82,6 @@ The new Modifiability Checker warns when a program might throw `UnsupportedOperationException` at run time because it calls a mutating method on a collection that does not support the method. -New `wpi2.sh` script does whole-program inference, but requires buildfile edits. - Removed deprecated script `checker/bin-devel/build.sh`; use `./gradlew assemble` instead. diff --git a/docs/manual/inference.tex b/docs/manual/inference.tex index a43d15ca36e4..2d3756c98536 100644 --- a/docs/manual/inference.tex +++ b/docs/manual/inference.tex @@ -105,20 +105,14 @@ least upper bound of the types of all the expressions that are assigned into the field. -There are four scripts that you can use to run whole-program inference. +There are three scripts that you can use to run whole-program inference. Each has advantages and disadvantages, discussed below: \begin{itemize} - \item - To run whole-program inference on a single project without modifying its - source code, use the \ script (Section~\ref{wpi-two}). This script - requires you to make a few changes to your buildfile. - \item To run whole-program inference on a single project without modifying its source code, use the \ script (Section~\ref{wpi-one}). This script can automatically understand many Ant, Maven, and Gradle build files, so it requires little manual configuration. - However, it is based on a no-longer-maintained project, \, so it is not guaranteed to work. \item To run whole-program inference on many projects without modifying their source code @@ -136,62 +130,7 @@ that applies to all of them. -\sectionAndLabel{Running whole-program inference on a single project, with buildfile editing}{wpi-two} - -You need to edit your buildfile, then run the \ script. - - -\subsectionAndLabel{Editing your buildfile}{wpi-two-buildfile} - -First, set up your buildfile to run the Checker Framework. - -Then, add the -\<-Aajava>, -\<-Ainfer>, -\<-AinferOutputDirectory>, and -\<-Awarns> command-line arguments. - -Also remove or comment out \<-Werror>, because the -\ script will halt if the Checker Framework fails, including when it -issues an error. - -Here is a Gradle example: - -\begin{Verbatim} -checkerFramework { - extraJavacArgs = [ - "-Ainfer=ajava", - "-AinferOutputOriginal", // optional - "-AinferOutputDirectory=" + project.projectDir + "/build/whole-program-inference", - "-Aajava=" + project.projectDir + "/whole-program-inference-output", - "-Awarns", - // "-Werror", // remove or comment out any occurrence of "-Werror". - ... - ] - ... -} -\end{Verbatim} - - -\subsectionAndLabel{Running \}{wpi-two-command} - -Second, run \. Its argument is the command that runs the Checker Framework. -Here is a typical invocation: - -\begin{Verbatim} - $CHECKERFRAMEWORK/checker/bin/wpi2.sh ./gradlew clean assemble -\end{Verbatim} - -The \ task is needed because \ runs the Checker Framework -repeatedly, and Gradle does nothing if the old (but still up-to-date) -compilation output is present. - -The output of \ is a top-level directory -\ that contains \<.ajava> files for each file of -your project. - - -\sectionAndLabel{Running whole-program inference on a single project, without buildfile editing}{wpi-one} +\sectionAndLabel{Running whole-program inference on a single project}{wpi-one} A typical invocation of \ is From 8dfb89849ae6a0a28a2969b1940b279099d7adfa Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 16 Sep 2026 15:10:17 -0700 Subject: [PATCH 289/308] Move the CHANGELOG entry to the unreleased section Co-Authored-By: Claude Opus 5 --- docs/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 52f48828da60..0f2bc1bd51d1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,10 @@ ### User-visible changes +The new Modifiability Checker warns when a program might throw +`UnsupportedOperationException` at run time because it calls a mutating method on +a collection that does not support the method. + The Purity Checker has been improved, so `-AcheckPurityAnnotations` may issue warnings that it did not previously. @@ -81,10 +85,6 @@ In `TypeHierarchy`: ### User-visible changes -The new Modifiability Checker warns when a program might throw -`UnsupportedOperationException` at run time because it calls a mutating method on -a collection that does not support the method. - Removed deprecated script `checker/bin-devel/build.sh`; use `./gradlew assemble` instead. From 978f854c0f14839a578cd72f88268efc0f5f077a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 20 Sep 2026 19:44:49 -0700 Subject: [PATCH 290/308] Code review --- .../org/checkerframework/afu/scenelib/el/ADeclaration.java | 4 ++++ .../java/org/checkerframework/afu/scenelib/el/AField.java | 2 ++ .../org/checkerframework/afu/scenelib/el/ATypeElement.java | 2 ++ .../checkerframework/dataflow/busyexpr/BusyExprStore.java | 5 +++-- docs/manual/modifiability-checker.tex | 6 +++--- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java index 0b5f0179ee65..9655c1c0f5ac 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java @@ -12,6 +12,8 @@ public abstract class ADeclaration extends AElement { /** * The element's insert-annotation invocations; map key is the AST path to the insertion place. */ + @SuppressWarnings("modifiability:assignment") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. public final @Modifiable VivifyingMap insertAnnotations = new VivifyingMap<>(new TreeMap<>()) { @Override @@ -29,6 +31,8 @@ public boolean isEmptyValue(ATypeElement v) { * The element's annotated insert-typecast invocations; map key is the AST path to the insertion * place */ + @SuppressWarnings("modifiability:assignment") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. public final @Modifiable VivifyingMap insertTypecasts = new VivifyingMap<>(new TreeMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java index 28174fdb31f8..e3287da6b08f 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java @@ -166,6 +166,8 @@ public R accept(ElementVisitor v, T t) { * @param the type of the map keys * @return a new vivifying map from keys to {@link AField}s */ + @SuppressWarnings("modifiability:return") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. static @Modifiable VivifyingMap newVivifyingLHMap_AF() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java index c38e5d7bda7c..c5dc82a3c549 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java @@ -121,6 +121,8 @@ public R accept(ElementVisitor v, T t) { * @param the type of the map keys * @return a new vivifying map from keys to {@link ATypeElement}s */ + @SuppressWarnings("modifiability:return") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. static @Modifiable VivifyingMap newVivifyingLHMap_ATE() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java index 0502fe0da4d1..1d7dcfade587 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java @@ -3,6 +3,7 @@ import java.util.LinkedHashSet; import java.util.Set; import java.util.StringJoiner; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.Store; @@ -16,7 +17,7 @@ public class BusyExprStore implements Store { /** A set of busy expression abstract values. */ - private final @Shrinkable Set busyExprValueSet; + private final @Growable @Shrinkable Set busyExprValueSet; /** * Create a new BusyExprStore. @@ -24,7 +25,7 @@ public class BusyExprStore implements Store { * @param busyExprValueSet a set of busy expression abstract values. The parameter is captured and * the caller should not retain an alias. */ - public BusyExprStore(@Shrinkable Set busyExprValueSet) { + public BusyExprStore(@Growable @Shrinkable Set busyExprValueSet) { this.busyExprValueSet = busyExprValueSet; } diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index ae6ca970dc5b..b6b37524c728 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -376,7 +376,7 @@ @PolyShrinkable Iterator iterator(@PolyShrinkable ArrayList this) { ... } } class CopyOnWriteArrayList { - @Unshrinkable Iterator iterator(@PolyShrinkable ArrayList this) { ... } + @Unshrinkable Iterator iterator(@PolyShrinkable CopyOnWriteArrayList this) { ... } } \end{Verbatim} @@ -476,7 +476,7 @@ structurally unavailable component to the corresponding top qualifier. For example, \ has \ but not \ or \, so \<@Modifiable Iterator> expands to -\<@MaybeGrowable @Shrinkable @MaybeReplaceable Iterator>. +\<@MaybeGrowable @MaybeSeqGrowable @Shrinkable @MaybeReplaceable Iterator>. \subsectionAndLabel{Alternative design: use negative capability rather than top qualifier}{modifiability-modifiable-must-be-top} @@ -603,7 +603,7 @@ @Shrinkable Iterator it = list.iterator(); it.remove(); // OK - @Unshrinkable List list2 = ...; + @IteratorPolyMod @Unshrinkable List list2 = ...; @Unshrinkable Iterator it2 = list2.iterator(); it2.remove(); // Error: it2 is @Unshrinkable From 7ecd75244060ad51cf09c622de1d761a01926a64 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 20 Sep 2026 20:42:12 -0700 Subject: [PATCH 291/308] Add Javadoc --- .../afu/scenelib/util/coll/VivifyingMap.java | 3 +++ .../checkerframework/common/basetype/BaseTypeVisitor.java | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index b6f18f716236..4acdae8e2188 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -49,6 +49,9 @@ public V getVivify(K k) { /** * Returns a new, "empty" value to which the key {@code k} can be mapped; subclasses must * implement. + * + * @param k the key to create a value for + * @return a new, "empty" value to which the key {@code k} can be mapped */ protected abstract V createValueFor(K k); 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 8dde2229c595..4e0e5912a9e6 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -3564,6 +3564,10 @@ private FoundRequired(AnnotatedTypeMirror found, AnnotatedTypeParameterBounds re /** * Creates string representations of {@link AnnotatedTypeMirror}s which are only verbose if * required to differentiate the two types. + * + * @param found the found type + * @param required the required type + * @return a FoundRequired for the two types */ public static FoundRequired of(AnnotatedTypeMirror found, AnnotatedTypeMirror required) { return new FoundRequired(found, required); @@ -3573,6 +3577,10 @@ public static FoundRequired of(AnnotatedTypeMirror found, AnnotatedTypeMirror re * Creates string representations of {@link AnnotatedTypeMirror} and {@link * AnnotatedTypeParameterBounds}s which are only verbose if required to differentiate the two * types. + * + * @param found the found type + * @param required the required bounds + * @return a FoundRequired for the type and the bounds */ public static FoundRequired of( AnnotatedTypeMirror found, AnnotatedTypeParameterBounds required) { From 71d6ad00370a7fc9bc71f0ac492624d5c1a6089f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 21 Sep 2026 08:47:30 -0700 Subject: [PATCH 292/308] Code review Make `checkModifiability` run to completion: strip `-Werror` when `-Awarns` is passed, and run the task from `typecheck-part1`. Weaken `@PolyModifiable` to `@MaybeSeqGrowable` on `Map.Entry`, as the Grow and Shrink Checkers already do for their own capabilities. Reset `ModifiabilityVisitor.inParameterType` in a `finally` clause. Recognize `UnsupportedOperationException` by its type rather than by the name written in the source code. Give every modifiability warning suppression a justification. Correct the alias expansion table in the manual, and document the `@PolyModifiable` expansion rule. --- .../afu/scenelib/el/AElement.java | 3 +- .../afu/scenelib/el/AScene.java | 3 +- build.gradle | 16 ++++++-- .../ModifiabilityBaseVisitor.java | 18 ++------- .../modifiability/ModifiabilityVisitor.java | 13 ++++-- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 12 ++++++ .../modifiability/PolyModifiableTest.java | 20 ++++++++++ .../tests/modifiability/ShadowedUoeTest.java | 40 +++++++++++++++++++ docs/manual/modifiability-checker.tex | 33 +++++++++------ 9 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 checker/tests/modifiability/ShadowedUoeTest.java diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java index 8395f5f8b119..4c55abead63e 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AElement.java @@ -204,7 +204,8 @@ public R accept(ElementVisitor v, T t) { * @param the type of the map keys * @return a new vivifying map from keys to {@link AElement}s */ - @SuppressWarnings("modifiability:return") + @SuppressWarnings("modifiability:return") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. static @Modifiable VivifyingMap newVivifyingLHMap_AE() { return new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java index 84ea6c39e8e1..fddc8c641d52 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java @@ -59,7 +59,8 @@ public class AScene implements Cloneable { public final Map> imports = new LinkedHashMap<>(); /** This scene's annotated classes; map key is class name. */ - @SuppressWarnings("modifiability:assignment") // TODO + @SuppressWarnings("modifiability:assignment") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. public final @Modifiable VivifyingMap classes = new VivifyingMap<>(new LinkedHashMap<>()) { @Override diff --git a/build.gradle b/build.gradle index 4896d6ffe428..3aaa07f316be 100644 --- a/build.gradle +++ b/build.gradle @@ -535,6 +535,15 @@ def createCheckTypeTask(projectName, taskName, checker, args = []) { "-AwarnRedundantAnnotations", ] options.compilerArgs += args + if (args.contains("-Awarns")) { + // `-Awarns` downgrades the checker's errors to warnings, but every JavaCompile task is + // given `-Werror`, which would turn the warnings back into errors and would halt + // compilation before most of the source code has been checked. This runs in `doFirst` + // so that it takes effect no matter when `-Werror` was added. + doFirst { + options.compilerArgs -= "-Werror" + } + } options.forkOptions.jvmArgs += ["-Xmx4g"] options.fork = true @@ -980,8 +989,9 @@ subprojects { [ // The Modifiability Checker is under development and this codebase is not yet free of // its errors, so `-Awarns` downgrades them to warnings. This means that the - // `checkModifiability` task always succeeds; it does not enforce anything. Remove - // `-Awarns` when the codebase issues no modifiability errors. + // `checkModifiability` task always succeeds; it enforces nothing, but it does detect a + // crash of the checker. Remove `-Awarns` when the codebase issues no modifiability + // errors. "-Awarns", "-Afilenames", ]) @@ -1201,7 +1211,7 @@ subprojects { tasks.register("typecheck-part1") { description = "Run the Checker Framework on itself (part 1)" group = "Verification" - dependsOn("checkCompilerMessages", "checkFormatter", "checkInterning", "checkOptional", "checkPurity") + dependsOn("checkCompilerMessages", "checkFormatter", "checkInterning", "checkModifiability", "checkOptional", "checkPurity") } tasks.register("typecheck-part2") { description = "Run the Checker Framework on itself (part 2)" diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 54e8f5d42994..10388f73702b 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -3,8 +3,6 @@ import com.sun.source.tree.BlockTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; @@ -28,6 +26,7 @@ import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** * Base visitor for the modifiability sub-checkers (Grow, SeqGrow, Shrink, Replace, and Iterator). @@ -315,20 +314,11 @@ private boolean implIsUOE(MethodTree method) { return false; } ExpressionTree exception = tt.getExpression(); - if (!(exception instanceof NewClassTree nct)) { + if (!(exception instanceof NewClassTree)) { return false; } - ExpressionTree identifier = nct.getIdentifier(); - if (identifier instanceof IdentifierTree it) { - // TODO: This can be fooled if a different UnsupportedOperationException is imported. - // A way to prevent that, is to check the type of exception: - // types.isSameType(TreeUtils.typeOf(exception), ...); - return it.getName().contentEquals("UnsupportedOperationException"); - } else if (identifier instanceof MemberSelectTree mst) { - // TODO: For efficiency, to avoid call to `toString()`, could walk down the MemberSelectTree. - return mst.toString().equals("java.lang.UnsupportedOperationException"); - } - return false; + return TypesUtils.isDeclaredOfName( + TreeUtils.typeOf(exception), "java.lang.UnsupportedOperationException"); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 13f63ac66b2d..307d2de8d066 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -43,7 +43,9 @@ protected ModifiabilityVisitor(ModifiabilityChecker checker) { * parameter type from a disallowed one elsewhere in the same method. * *

    The field needs no save-and-restore discipline, because a method declaration cannot appear - * within a formal parameter. + * within a formal parameter. It is reset in a {@code finally} clause because this visitor is + * reused for every compilation unit, and a stale true value would suppress every subsequent + * {@code unmodparam.location} error. */ @Override public Void visitMethod(MethodTree tree, Void p) { @@ -53,9 +55,12 @@ public Void visitMethod(MethodTree tree, Void p) { scan(tree.getReturnType(), p); scan(tree.getTypeParameters(), p); inParameterType = true; - scan(tree.getParameters(), p); - scan(tree.getReceiverParameter(), p); - inParameterType = false; + try { + scan(tree.getParameters(), p); + scan(tree.getReceiverParameter(), p); + } finally { + inParameterType = false; + } scan(tree.getThrows(), p); scan(tree.getBody(), p); scan(tree.getDefaultValue(), p); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index d9aac183423d..11e7897404ed 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -128,4 +128,16 @@ protected boolean typeLacksCapability(TypeMirror type) { || TypesUtils.isErasedSubtype(type, dequeErasure, types); return !canSeqGrow; } + + /** + * For {@code Map.Entry}, only the replace bit is meaningful to carry from the map receiver, so + * the sequenced-grow bit of {@code @PolyModifiable} is {@code @MaybeSeqGrowable}. + */ + @Override + protected boolean polyLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); + } } diff --git a/checker/tests/modifiability/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java index 8941b797aed7..5cce419f56e3 100644 --- a/checker/tests/modifiability/PolyModifiableTest.java +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -1,8 +1,10 @@ import java.util.Deque; import java.util.List; +import java.util.Map; import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.MaybeSeqGrowable; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PolyModifiable; @@ -31,6 +33,12 @@ public class PolyModifiableTest { return x; } + /** A Map.Entry-specific identity method; only the replace capability is carried. */ + Map.@PolyModifiable Entry identityEntry( + Map.@PolyModifiable Entry x) { + return x; + } + void testPoly( @Modifiable List mod, @Modifiable @IteratorPolyMod List modIteratorPoly, @@ -156,4 +164,16 @@ void testPolySeqGrowOnDeque( // :: error: [assignment] @SeqGrowable Deque unknown2 = identityDeque(unknown); // Error: SeqGrow is unknown. } + + void testPolyOnEntry( + Map.@Modifiable Entry mod, Map.@SeqGrowable Entry seqGrow) { + + // A Map.Entry has no grow, seq-grow, or shrink methods, so @PolyModifiable carries only the + // replace capability, just as @Modifiable expands to only the replace capability. + Map.@Replaceable Entry e1 = identityEntry(mod); // OK + + Map.@MaybeSeqGrowable Entry e2 = identityEntry(seqGrow); // OK + // :: error: [assignment] + Map.@SeqGrowable Entry e3 = identityEntry(seqGrow); + } } diff --git a/checker/tests/modifiability/ShadowedUoeTest.java b/checker/tests/modifiability/ShadowedUoeTest.java new file mode 100644 index 000000000000..39f6670ad4cc --- /dev/null +++ b/checker/tests/modifiability/ShadowedUoeTest.java @@ -0,0 +1,40 @@ +// Tests that the `method.implementation.not.uoe` check compares the type of the thrown exception +// rather than the name that appears in the source code. + +import java.util.AbstractList; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; + +public class ShadowedUoeTest { + + /** + * A different exception that happens to be named UnsupportedOperationException. It shadows + * java.lang.UnsupportedOperationException throughout this class. + */ + static class UnsupportedOperationException extends RuntimeException {} + + // All constructors are @Ungrowable, so a method with a @Growable receiver must throw + // java.lang.UnsupportedOperationException. + static class UngrowableList extends AbstractList { + @Ungrowable UngrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + // :: error: [method.implementation.not.uoe] + public void growThrowsShadowed(@Growable UngrowableList this) { + throw new UnsupportedOperationException(); + } + + public void growThrowsQualified(@Growable UngrowableList this) { + throw new java.lang.UnsupportedOperationException(); + } + } +} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index b6b37524c728..67bfc0b0ad6b 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -508,32 +508,30 @@ \subsectionAndLabel{Expansion rules for \<@Modifiable> and \<@Unmodifiable>}{modifiability-modifiable-expansion-rules} -The following rules define this weakening. The first qualifier in each -capability row is used for \<@Modifiable>; the second is used for -\<@Unmodifiable>. If the weakening condition holds, both aliases use the -corresponding \<@Maybe*> qualifier instead. +The following rules define this weakening. A capability is weakened to the +corresponding \<@Maybe*> qualifier when the type satisfies that capability's +condition in the table below. For every other type, \<@Modifiable> expands to +the capability's positive qualifier, such as \<@Growable>, and +\<@Unmodifiable> expands to the capability's negative qualifier, such as +\<@Ungrowable>. \noindent -\begin{tabular}{l|l|l} -\multicolumn{3}{c}{Expansion for +\begin{tabular}{l|l} +\multicolumn{2}{c}{Weakening conditions for \<@Modifiable>/\<@Unmodifiable>. ``$\backslash$'' means set difference. A type includes its subtypes.} \\ \hline -& Usual expansion to \<@>(\)\<*> & Expands to \<@Maybe*> \\ \hline +& Expands to \<@Maybe*> \\ \hline Grow -& \ & \ or (\$\backslash$\) \\ SeqGrow -& \, \, or \ & not \, \, or \ \\ Shrink -& not \ & \ \\ Replace -& \ or \ & exact \, any \, (\$\backslash$\), \\ -&& or (\$\backslash$\) \\ +& or (\$\backslash$\) \\ \end{tabular} These rules describe alias expansion, not the full specification of every JDK @@ -577,6 +575,17 @@ \end{smaller} +\subsectionAndLabel{Expansion rules for \<@PolyModifiable>}{modifiability-polymodifiable-expansion-rules} + +\<@PolyModifiable> is weakened in only one case: on \, its grow, +seq-grow, and shrink components are the corresponding \<@Maybe*> qualifiers, +because no \ has methods for those capabilities. Every other type +keeps all four polymorphic qualifiers, even a type such as \ or +\ whose \<@Modifiable> expansion is weakened, because a variable of +such a type may hold a value of a subtype, such as \ or +\, that does have the methods. + + \sectionAndLabel{Behavior of the \ method}{modifiability-iterator-behavior} By default, the signature of \ is From 66d1e8b6afd93f5209d5df5f676ed2958106f024 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 21 Sep 2026 10:04:59 -0700 Subject: [PATCH 293/308] Fix iteration test --- checker/tests/modifiability/CollectionsModifiableTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java index d221179c15b1..921122f50b59 100644 --- a/checker/tests/modifiability/CollectionsModifiableTest.java +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -116,7 +116,11 @@ void testEmptyCollections() { // :: error: [assignment] @Modifiable Map modM = Collections.emptyMap(); - @Modifiable Iterator it = Collections.emptyIterator(); + @Unmodifiable Iterator itor = Collections.emptyIterator(); + // :: error: [method.invocation] + itor.remove(); + // :: error: [assignment] + @Modifiable Iterator modItor = Collections.emptyIterator(); } void testFill() { From 7ce10a02a64dd69f418cc228cb82947f81418c91 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 22 Sep 2026 11:04:01 -0700 Subject: [PATCH 294/308] Address code review findings on the Modifiability Checker Allow @UnmodifiableParam on an explicitly-typed lambda parameter. The aggregate visitor set `inParameterType` only in `visitMethod`, so a lambda's parameters were scanned as if they were not parameters at all. Check the methods that a class inherits without overriding, not only the ones it declares. A class whose constructors claim a capability, such as @Growable, cannot inherit a method whose implementation always throws UnsupportedOperationException; otherwise the call type-checks and throws at run time. The body of an inherited method is compiled separately, so the new @ThrowsUnsupportedOperation annotation records the fact where the implementation is written. A new stub file writes it on the JDK's skeletal implementations, and the checker verifies it on every method it compiles. Accept a subclass of UnsupportedOperationException in a method body. The check compared the name of the thrown type, which a genuine subclass does not match; it now compares the type, which still rejects an unrelated class that shadows the name. Remove the @SuppressWarnings("class.unverified") annotations and the commented-out manual section that were left behind when the `class.unverified` warning was removed. The key is in no messages.properties, and because it contains no colon, -AwarnUnneededSuppressions could not report it as unneeded. Rename CustomModifiabilityAnnotationWarning.java, which is named for that removed warning, to CustomCollectionAnnotations.java. --- .../qual/ThrowsUnsupportedOperation.java | 32 +++++ .../ModifiabilityBaseChecker.java | 3 +- .../ModifiabilityBaseVisitor.java | 111 +++++++++++++++-- .../modifiability/ModifiabilityVisitor.java | 52 +++++++- .../checker/modifiability/jdk.astub | 37 ++++++ .../checker/modifiability/messages.properties | 4 +- .../ConstructorResultTypeTest.java | 10 +- ....java => CustomCollectionAnnotations.java} | 8 +- .../ExactReceiverOverrideTest.java | 1 - .../InheritedImplementationTest.java | 115 ++++++++++++++++++ .../modifiability/UnmodParamLocationTest.java | 12 +- .../modifiability/UoeImplementationTest.java | 6 + docs/manual/modifiability-checker.tex | 77 +++++++----- .../tests/all-systems/GenericsCasts.java | 1 - .../tests/all-systems/GenericsEnclosing.java | 1 - .../tests/all-systems/RawTypeAssignment.java | 1 - .../all-systems/java8inference/Issue3036.java | 1 - 17 files changed, 419 insertions(+), 53 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUnsupportedOperation.java create mode 100644 checker/src/main/java/org/checkerframework/checker/modifiability/jdk.astub rename checker/tests/modifiability/{CustomModifiabilityAnnotationWarning.java => CustomCollectionAnnotations.java} (92%) create mode 100644 checker/tests/modifiability/InheritedImplementationTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUnsupportedOperation.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUnsupportedOperation.java new file mode 100644 index 000000000000..34e5ffe0f0fc --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/ThrowsUnsupportedOperation.java @@ -0,0 +1,32 @@ +package org.checkerframework.checker.modifiability.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; + +/** + * Indicates that the method's implementation always throws {@link UnsupportedOperationException}, + * as {@code AbstractList.set()} does. + * + *

    Write this annotation on a skeletal implementation, such as one in {@code + * java.util.AbstractList}, that a subclass is expected to override. A subclass that inherits the + * implementation without overriding it does not support the operation, even if the subclass + * declares that it does. The Modifiability Checker issues an error for such a subclass; without + * this annotation, it could not, because the body of an inherited method is compiled separately and + * the checker sees only its signature. + * + *

    The Modifiability Checker verifies the annotation on any method it compiles: the body must be + * exactly {@code throw new UnsupportedOperationException(...)}. + * + *

    Do not write this annotation on a method that throws {@link UnsupportedOperationException} + * only because some other method does, such as {@code AbstractList.add(E)}, whose body is {@code + * add(size(), e)}. A subclass that overrides the other method makes such a method work. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ThrowsUnsupportedOperation {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index 569fa107492b..782e2e0c2b84 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -35,7 +35,8 @@ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { private static final List SHARED_STUB_FILES = List.of( "/org/checkerframework/checker/modifiability/ical4j.astub", - "/org/checkerframework/checker/modifiability/javaparser.astub"); + "/org/checkerframework/checker/modifiability/javaparser.astub", + "/org/checkerframework/checker/modifiability/jdk.astub"); /** Creates a new ModifiabilityBaseChecker. */ protected ModifiabilityBaseChecker() {} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 10388f73702b..3aeef450b029 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -16,7 +16,11 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.modifiability.qual.ThrowsUnsupportedOperation; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -25,6 +29,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -43,6 +48,8 @@ * requiring the body of each method that requires the capability -- either on its own * receiver parameter or from a method that it overrides -- to agree with that qualifier about * whether the method throws {@link UnsupportedOperationException}. + *

  • Requiring a class that claims the capability not to inherit, without overriding, a method + * whose implementation always throws {@link UnsupportedOperationException}. *
  • Requiring an override to preserve a positive receiver capability of the method it * overrides. * @@ -71,9 +78,8 @@ public ModifiabilityBaseVisitor(BaseTypeChecker checker) { * *

    What makes the suppression less unsafe is that the declared modifiability of a class is * checked against its method bodies; see {@link #processClassMembers}. That check is not - * complete, so the suppression does permit some unsound code. The check does nothing unless some - * constructor of the class declares a qualifier in this hierarchy, and it examines only the - * methods that the class declares, not those that it inherits without overriding. + * complete, so the suppression does permit some unsound code: it does nothing unless some + * constructor of the class declares a qualifier in this hierarchy. */ @Override protected void checkThisOrSuperConstructorCall( @@ -163,6 +169,85 @@ private void processClassMembers(ClassTree tree) { checkImplOK(method, receiverAnno, constructorAnno); } } + + checkInheritedImplementations(tree, classElement, constructorAnno); + } + + /** + * Issues an error if the class claims this checker's capability, but inherits without overriding + * a method whose implementation always throws {@link UnsupportedOperationException}. + * + *

    The body of an inherited method is usually not available -- it is compiled separately, and + * the checker sees only its signature -- so the implementation is known to throw only if it is + * annotated {@code @}{@link ThrowsUnsupportedOperation}, as {@code AbstractList.set()} is, or if + * it is declared in a class whose constructors declare the negative qualifier, in which case + * {@link #checkImplOK} verified that it throws. + * + * @param tree a class + * @param classElement the element for {@code tree} + * @param constructorAnno the result qualifier that the class's constructors declare + */ + private void checkInheritedImplementations( + ClassTree tree, TypeElement classElement, AnnotationMirror constructorAnno) { + if (!atypeFactory.hasNegativeCapability() + || !AnnotationUtils.areSameByName(constructorAnno, positiveCapability())) { + // The class does not claim the capability, so an inherited implementation that throws + // UnsupportedOperationException agrees with what the class says about itself. (In the + // Iterator hierarchy, which has no negative qualifier, no implementation throws + // UnsupportedOperationException on account of this checker's capability.) + return; + } + for (ExecutableElement method : ElementFilter.methodsIn(elements.getAllMembers(classElement))) { + TypeElement declaringClass = ElementUtils.enclosingTypeElement(method); + if (declaringClass == null || declaringClass.equals(classElement)) { + // A method that this class declares was checked against its own body, above. + continue; + } + if (!implementationThrowsUOE(method, declaringClass)) { + continue; + } + AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); + if (receiverType == null || !receiverType.hasPrimaryAnnotation(positiveCapability())) { + continue; + } + checker.reportError( + tree, + "inherited.implementation.uoe", + method, + declaringClass, + constructorAnno.getAnnotationType().asElement().getSimpleName()); + } + } + + /** + * Returns true if the implementation of {@code method}, which {@code declaringClass} declares, + * always throws {@link UnsupportedOperationException}. + * + * @param method a method that some other class inherits + * @param declaringClass the class that declares {@code method} + * @return true if the implementation of {@code method} always throws {@link + * UnsupportedOperationException} + */ + private boolean implementationThrowsUOE(ExecutableElement method, TypeElement declaringClass) { + if (atypeFactory.getDeclAnnotation(method, ThrowsUnsupportedOperation.class) != null) { + return true; + } + // A class whose constructors declare the negative qualifier, such as @Ungrowable, was itself + // checked: every method of it that requires the capability throws + // UnsupportedOperationException. A class that declares nothing, such as AbstractList, says + // nothing about its methods, and only the @ThrowsUnsupportedOperation annotation does. + for (ExecutableElement constructor : + ElementFilter.constructorsIn(declaringClass.getEnclosedElements())) { + AnnotationMirror resultAnno = + atypeFactory + .getAnnotatedType(constructor) + .getReturnType() + .getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); + if (resultAnno != null && isNegativeCapability(resultAnno)) { + return true; + } + } + return false; } /** @@ -267,11 +352,11 @@ private void checkImplOK( String constructorAnnoName = constructorAnno.getAnnotationType().asElement().getSimpleName().toString(); if (isNegativeCapability(constructorAnno)) { - if (!implIsUOE(method)) { + if (!implIsUOE(method, types, elements)) { checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); } } else if (AnnotationUtils.areSameByName(constructorAnno, positiveCapability())) { - if (implIsUOE(method)) { + if (implIsUOE(method, types, elements)) { checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); } } @@ -294,12 +379,15 @@ private boolean isNegativeCapability(AnnotationMirror anno) { /** * Returns true if the method body is exactly {@code throw new - * UnsupportedOperationException(...)}. + * UnsupportedOperationException(...)}, where the exception is {@link + * UnsupportedOperationException} or a subclass of it. * * @param method a method declaration + * @param types the type utilities + * @param elements the element utilities * @return true if the method body is exactly {@code throw new UnsupportedOperationException(...)} */ - private boolean implIsUOE(MethodTree method) { + static boolean implIsUOE(MethodTree method, Types types, Elements elements) { BlockTree body = method.getBody(); if (body == null) { // The method is abstract or native, so it has no body. @@ -317,8 +405,13 @@ private boolean implIsUOE(MethodTree method) { if (!(exception instanceof NewClassTree)) { return false; } - return TypesUtils.isDeclaredOfName( - TreeUtils.typeOf(exception), "java.lang.UnsupportedOperationException"); + // Compare the type rather than the name, so that a class that shadows + // java.lang.UnsupportedOperationException does not satisfy the check, but a subclass of + // java.lang.UnsupportedOperationException does. + TypeMirror unsupportedOperationException = + TypesUtils.typeFromClass(UnsupportedOperationException.class, types, elements); + return types.isSubtype( + types.erasure(TreeUtils.typeOf(exception)), types.erasure(unsupportedOperationException)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 307d2de8d066..08f0665cfcc7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -1,11 +1,13 @@ package org.checkerframework.checker.modifiability; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodTree; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; +import org.checkerframework.checker.modifiability.qual.ThrowsUnsupportedOperation; import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; import org.checkerframework.framework.source.SourceVisitor; import org.checkerframework.javacutil.AnnotationUtils; @@ -42,15 +44,16 @@ protected ModifiabilityVisitor(ModifiabilityChecker checker) { * that {@link #visitAnnotation} can distinguish an allowed {@code @UnmodifiableParam} in a * parameter type from a disallowed one elsewhere in the same method. * - *

    The field needs no save-and-restore discipline, because a method declaration cannot appear - * within a formal parameter. It is reset in a {@code finally} clause because this visitor is - * reused for every compilation unit, and a stale true value would suppress every subsequent - * {@code unmodparam.location} error. + *

    The field needs no save-and-restore discipline, because neither a method declaration nor a + * lambda expression can appear within a formal parameter. It is reset in a {@code finally} clause + * because this visitor is reused for every compilation unit, and a stale true value would + * suppress every subsequent {@code unmodparam.location} error. */ @Override public Void visitMethod(MethodTree tree, Void p) { storeSuppressWarningsAnno(tree); checkPreservesModifiabilityLocation(tree); + checkThrowsUnsupportedOperation(tree); scan(tree.getModifiers(), p); scan(tree.getReturnType(), p); scan(tree.getTypeParameters(), p); @@ -67,6 +70,47 @@ public Void visitMethod(MethodTree tree, Void p) { return null; } + /** + * Sets {@link #inParameterType} while scanning this lambda's formal parameters, so that an + * explicitly-typed lambda parameter may be annotated {@code @UnmodifiableParam}, just as a method + * formal parameter may. An implicitly-typed lambda parameter has no type to annotate. + */ + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) { + inParameterType = true; + try { + scan(tree.getParameters(), p); + } finally { + inParameterType = false; + } + scan(tree.getBody(), p); + return null; + } + + /** + * Issues an error if {@code tree} is annotated {@code @ThrowsUnsupportedOperation} but its body + * is not exactly {@code throw new UnsupportedOperationException(...)}. + * + *

    The annotation is a promise that other classes rely on: a class that inherits the method + * without overriding it does not support the operation. A method with no body, such as an + * abstract method, has no implementation to make the promise about, so it is an error too. + * + *

    This check is in the aggregate checker rather than in {@link ModifiabilityBaseVisitor} + * because it does not depend on a modifiability hierarchy; running it in each sub-checker would + * issue the same error five times. + * + * @param tree a method declaration + */ + private void checkThrowsUnsupportedOperation(MethodTree tree) { + ExecutableElement methodElt = TreeUtils.elementFromDeclaration(tree); + if (methodElt == null || methodElt.getAnnotation(ThrowsUnsupportedOperation.class) == null) { + return; + } + if (!ModifiabilityBaseVisitor.implIsUOE(tree, types, elements)) { + checker.reportError(tree, "throwsuoe.implementation.not.uoe"); + } + } + /** * Issues an error if {@code tree} is annotated as {@code @PreservesModifiability} but is not a * method that has exactly one formal parameter, which is not a varargs parameter, and a non-void diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/jdk.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/jdk.astub new file mode 100644 index 000000000000..c1e7a8c720dc --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/jdk.astub @@ -0,0 +1,37 @@ +// Methods of the JDK's skeletal collection classes whose implementation always throws +// UnsupportedOperationException. A subclass that inherits one of them, without overriding it, does +// not support the operation. +// +// Do not list a method that throws UnsupportedOperationException only because some other method +// does. For example, `AbstractList.add(E)` has the body `add(size(), e)`, and +// `AbstractCollection.addAll()` calls `add()` on each element; a subclass that overrides +// `AbstractList.add(int, E)` makes both of them work. +// +// A stub file overrides the annotated JDK for every method it mentions, so each method below +// repeats the modifiability qualifiers that the annotated JDK writes on it. Keep them in sync. + +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.ThrowsUnsupportedOperation; + +package java.util; + +class AbstractCollection { + @ThrowsUnsupportedOperation boolean add(@IteratorPolyMod @Growable AbstractCollection this, E e); +} + +class AbstractList { + @ThrowsUnsupportedOperation void add(@IteratorPolyMod @Growable AbstractList this, int index, E element); + @ThrowsUnsupportedOperation E set(@IteratorPolyMod @Replaceable AbstractList this, int index, E element); + @ThrowsUnsupportedOperation E remove(@IteratorPolyMod @Shrinkable AbstractList this, int index); +} + +class AbstractMap { + @ThrowsUnsupportedOperation V put(@Growable @Replaceable AbstractMap this, K key, V value); +} + +interface Iterator { + @ThrowsUnsupportedOperation void remove(@Shrinkable Iterator this); +} diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties index 98b927329486..bcf7df473b8d 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -1,6 +1,8 @@ -unmodparam.location=@UnmodifiableParam may only be written within the type of a method or constructor formal parameter, including the receiver parameter +unmodparam.location=@UnmodifiableParam may only be written within the type of a formal parameter of a method, constructor, or lambda expression, including the receiver parameter preservesmodifiability.location=@PreservesModifiability may only be written on a method declaration that has exactly one formal parameter, which is not a varargs parameter, and a non-void result inconsistent.constructor.result.type=Constructor result type %s is inconsistent with a previous constructor with result type %s bottom.annotation.on.receiver=Do not write the bottom annotation on a receiver method.implementation.is.uoe=Method implementation should not be `throw new UnsupportedOperationException()`, because the constructors of this class are %s method.implementation.not.uoe=Method implementation should be `throw new UnsupportedOperationException()`, because the constructors of this class are %s +inherited.implementation.uoe=The implementation of %s that this class inherits from %s throws UnsupportedOperationException, but the constructors of this class are %s. Override the method, or change the constructor annotation. +throwsuoe.implementation.not.uoe=The implementation of a method annotated @ThrowsUnsupportedOperation must be `throw new UnsupportedOperationException()` diff --git a/checker/tests/modifiability/ConstructorResultTypeTest.java b/checker/tests/modifiability/ConstructorResultTypeTest.java index 8d5e3abf0716..94c7dbd94fdb 100644 --- a/checker/tests/modifiability/ConstructorResultTypeTest.java +++ b/checker/tests/modifiability/ConstructorResultTypeTest.java @@ -1,5 +1,9 @@ // Tests the `inconsistent.constructor.result.type` and `bottom.annotation.on.receiver` checks, // which are performed by ModifiabilityBaseVisitor.processClassTree. +// +// A class below whose constructors are @Growable also gets an `inherited.implementation.uoe` +// error, because it extends AbstractList without overriding `add(int, E)`, whose implementation +// throws UnsupportedOperationException. See InheritedImplementationTest.java. import java.util.AbstractList; import java.util.List; @@ -9,7 +13,9 @@ public class ConstructorResultTypeTest { - // All the constructors declare the same result qualifier, so there is no error. + // All the constructors declare the same result qualifier, so there is no + // `inconsistent.constructor.result.type` error. + // :: error: [inherited.implementation.uoe] static class ConsistentConstructors extends AbstractList { @Growable ConsistentConstructors() {} @@ -45,6 +51,7 @@ public int size() { } // The bottom qualifier is meaningless on a receiver, so writing it is an error. + // :: error: [inherited.implementation.uoe] static class BottomReceiver extends AbstractList { @Growable BottomReceiver() {} @@ -91,6 +98,7 @@ public void bottomReceiver(@BottomGrowable InconsistentConstructorsAndBottomRece } // The method has no body, but the bottom qualifier on its receiver is still an error. + // :: error: [inherited.implementation.uoe] abstract static class AbstractBottomReceiver extends AbstractList { @Growable AbstractBottomReceiver() {} diff --git a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java b/checker/tests/modifiability/CustomCollectionAnnotations.java similarity index 92% rename from checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java rename to checker/tests/modifiability/CustomCollectionAnnotations.java index 67c893f1b039..d437be5adb58 100644 --- a/checker/tests/modifiability/CustomModifiabilityAnnotationWarning.java +++ b/checker/tests/modifiability/CustomCollectionAnnotations.java @@ -20,7 +20,7 @@ import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; -public class CustomModifiabilityAnnotationWarning { +public class CustomCollectionAnnotations { static class ExtendsAbstractList extends AbstractList { @Override @@ -44,6 +44,8 @@ abstract static class DirectListIterator implements ListIterator {} static class NonModifiabilityType {} + // :: error: [inherited.implementation.uoe] :: error: [inherited.implementation.uoe] :: error: + // [inherited.implementation.uoe] static @Modifiable class ClassLevelModifiableList extends AbstractList { @Override public String get(int index) { @@ -72,6 +74,7 @@ static class MapWithAnnotatedKeySet extends AbstractMap { throw new UnsupportedOperationException(); } + // :: error: [inherited.implementation.uoe] class KeySet extends AbstractSet { public @Modifiable KeySet() {} @@ -91,6 +94,7 @@ public int size() { abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator implements Iterator {} + // :: error: [inherited.implementation.uoe] static class ConstructorLevelShrinkableList extends AbstractList { @Shrinkable ConstructorLevelShrinkableList() {} @@ -151,6 +155,8 @@ public int size() { } } + // :: error: [inherited.implementation.uoe] :: error: [inherited.implementation.uoe] :: error: + // [inherited.implementation.uoe] static @Modifiable class SuppressedList extends AbstractList { @Override public String get(int index) { diff --git a/checker/tests/modifiability/ExactReceiverOverrideTest.java b/checker/tests/modifiability/ExactReceiverOverrideTest.java index 898bebd5b0ce..6c19557a81a9 100644 --- a/checker/tests/modifiability/ExactReceiverOverrideTest.java +++ b/checker/tests/modifiability/ExactReceiverOverrideTest.java @@ -9,7 +9,6 @@ import org.checkerframework.checker.modifiability.qual.Shrinkable; import org.checkerframework.checker.modifiability.qual.Unmodifiable; -@SuppressWarnings("class.unverified") public class ExactReceiverOverrideTest { static class GrowParent extends ArrayList { diff --git a/checker/tests/modifiability/InheritedImplementationTest.java b/checker/tests/modifiability/InheritedImplementationTest.java new file mode 100644 index 000000000000..1cddf28eed6b --- /dev/null +++ b/checker/tests/modifiability/InheritedImplementationTest.java @@ -0,0 +1,115 @@ +// Tests the `inherited.implementation.uoe` check, which is about a method whose implementation the +// class inherits without overriding, and the `throwsuoe.implementation.not.uoe` check, which +// verifies the @ThrowsUnsupportedOperation annotation itself. + +import java.util.AbstractList; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.ThrowsUnsupportedOperation; +import org.checkerframework.checker.modifiability.qual.Ungrowable; + +public class InheritedImplementationTest { + + // `AbstractList.add(int, E)` is annotated @ThrowsUnsupportedOperation, and this class overrides + // it, so the class does support growing. + static class GrowableList extends AbstractList { + @Growable GrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + @Override + public void add(@Growable GrowableList this, int index, String element) {} + } + + // All the constructors are @Ungrowable, so `growThrows()` was checked to throw + // UnsupportedOperationException. + static class UngrowableList extends AbstractList { + @Ungrowable UngrowableList() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void growThrows(@Growable UngrowableList this) { + throw new UnsupportedOperationException(); + } + } + + // The class inherits `growThrows()`, which throws, and `AbstractList.add(int, E)`, which is + // annotated @ThrowsUnsupportedOperation. It cannot claim to be @Growable. + // :: error: [inherited.implementation.uoe] :: error: [inherited.implementation.uoe] + static class GrowableSubclass extends UngrowableList { + @Growable GrowableSubclass() {} + } + + // Overriding both of them is enough. + static class GrowableSubclassThatOverrides extends UngrowableList { + @Growable GrowableSubclassThatOverrides() {} + + @Override + public void growThrows(@Growable GrowableSubclassThatOverrides this) {} + + @Override + public void add(@Growable GrowableSubclassThatOverrides this, int index, String element) {} + } + + /** A subclass of UnsupportedOperationException is an UnsupportedOperationException. */ + static class MyUnsupportedOperationException extends UnsupportedOperationException {} + + // A method whose body throws a subclass of UnsupportedOperationException does not support the + // operation, just as one that throws UnsupportedOperationException itself does not. + static class ThrowsSubclass extends AbstractList { + @Ungrowable ThrowsSubclass() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void growThrows(@Growable ThrowsSubclass this) { + throw new MyUnsupportedOperationException(); + } + } + + // The checker verifies @ThrowsUnsupportedOperation on every method that it compiles. + abstract static class AnnotationVerification extends AbstractList { + @Ungrowable AnnotationVerification() {} + + @ThrowsUnsupportedOperation + public void throwsUoe(@Growable AnnotationVerification this) { + throw new UnsupportedOperationException(); + } + + @ThrowsUnsupportedOperation + public void throwsSubclassOfUoe(@Growable AnnotationVerification this) { + throw new MyUnsupportedOperationException(); + } + + @ThrowsUnsupportedOperation + // :: error: [throwsuoe.implementation.not.uoe] :: error: [method.implementation.not.uoe] + public void doesNotThrow(@Growable AnnotationVerification this) {} + + // An abstract method has no implementation to make the promise about. + @ThrowsUnsupportedOperation + // :: error: [throwsuoe.implementation.not.uoe] + public abstract void noBody(@Growable AnnotationVerification this); + } +} diff --git a/checker/tests/modifiability/UnmodParamLocationTest.java b/checker/tests/modifiability/UnmodParamLocationTest.java index b5b1b28bd167..0e7836901148 100644 --- a/checker/tests/modifiability/UnmodParamLocationTest.java +++ b/checker/tests/modifiability/UnmodParamLocationTest.java @@ -18,7 +18,6 @@ void method(@UnmodifiableParam List parameter) {} void nestedParameter(List<@UnmodifiableParam List> parameter) {} - @SuppressWarnings("class.unverified") abstract static class RelevantReceiver implements List { void receiver(@UnmodifiableParam RelevantReceiver this) {} } @@ -40,11 +39,20 @@ void local(Object object) { } void lambda() { - // :: error: [unmodparam.location] Consumer> consumer = (@UnmodifiableParam List parameter) -> {}; consumer.accept(null); } + void lambdaBody() { + Consumer> consumer = + (List parameter) -> { + // :: error: [unmodparam.location] + @UnmodifiableParam List local = null; + local = null; + }; + consumer.accept(null); + } + // :: error: [unmodparam.location] <@UnmodifiableParam S> void methodTypeParameter(S value) {} } diff --git a/checker/tests/modifiability/UoeImplementationTest.java b/checker/tests/modifiability/UoeImplementationTest.java index 42f6bfb83906..073a53618243 100644 --- a/checker/tests/modifiability/UoeImplementationTest.java +++ b/checker/tests/modifiability/UoeImplementationTest.java @@ -1,5 +1,9 @@ // Tests the `method.implementation.is.uoe` and `method.implementation.not.uoe` checks, which // compare a method body against the modifiability annotation on the class's constructors. +// +// A class below whose constructors are @Growable also gets an `inherited.implementation.uoe` +// error, because it extends AbstractList without overriding `add(int, E)`, whose implementation +// throws UnsupportedOperationException. See InheritedImplementationTest.java. import java.util.AbstractList; import org.checkerframework.checker.modifiability.qual.Growable; @@ -35,6 +39,7 @@ public void growDoesNotThrow(@Growable UngrowableList this) {} // All constructors are @Growable, so a method with a @Growable receiver must not throw // UnsupportedOperationException. + // :: error: [inherited.implementation.uoe] static class GrowableList extends AbstractList { @Growable GrowableList() {} @@ -59,6 +64,7 @@ public void growThrows(@Growable GrowableList this) { // `add()` states no requirement on its own receiver, but it inherits one from `List.add()`, // which it overrides. All the constructors are @Growable, so `add()` must not throw // UnsupportedOperationException. + // :: error: [inherited.implementation.uoe] static @Growable class ClassLevelGrowableList extends AbstractList { @Growable ClassLevelGrowableList() {} diff --git a/docs/manual/modifiability-checker.tex b/docs/manual/modifiability-checker.tex index 67bfc0b0ad6b..5aab22c7344a 100644 --- a/docs/manual/modifiability-checker.tex +++ b/docs/manual/modifiability-checker.tex @@ -460,8 +460,56 @@ the first argument of a call to a varargs method is an element of the varargs array rather than the sole formal parameter. +\item[\refqualclass{checker/modifiability/qual}{ThrowsUnsupportedOperation}] +Indicates that the method's implementation always throws +\UnsupportedOperationException. Write it on a skeletal implementation, such +as one in \, that a subclass is expected to override. + +A subclass that inherits the implementation, without overriding it, does not +support the operation, even if the subclass declares that it does. The +Modifiability Checker issues an error for such a subclass; see +Section~\ref{modifiability-inherited-implementations}. Without the +annotation, it could not, because the body of an inherited method is compiled +separately and the checker sees only its signature. + +The Modifiability Checker verifies the annotation on every method that it +compiles: the body must be exactly +\. + +Do not write the annotation on a method that throws +\UnsupportedOperationException\ only because some other method does, such as +\, whose body is \. A subclass that +overrides the other method makes such a method work. + \end{description} +\subsectionAndLabel{Inherited implementations}{modifiability-inherited-implementations} + +The Modifiability Checker requires the constructors of a class to agree with +the class's method bodies: if the constructors are \<@Ungrowable>, then every +method with a \<@Growable> receiver must throw \UnsupportedOperationException, +and if the constructors are \<@Growable>, then no such method may. + +A class does not escape the requirement by inheriting a method rather than +declaring it. For example, this class does not support \, because +\ implements \ by throwing +\UnsupportedOperationException: + +\begin{Verbatim} + // error: the inherited implementation of add(int, E) throws + class GrowableList extends AbstractList { + @Growable GrowableList() {} + ... + } +\end{Verbatim} + +The checker knows that an inherited implementation throws +\UnsupportedOperationException\ if the method is annotated +\<@ThrowsUnsupportedOperation>, or if the method's class declares the negative +qualifier on its constructors, in which case the checker already verified that +the method throws. The checker cannot tell for any other inherited method, so +it says nothing about one. + \sectionAndLabel{\<@Modifiable> and \<@Unmodifiable> for types without grow, seq-grow, shrink, and/or replace methods}{modifiability-types-lacking-methods} Ordinarily, the declaration @@ -621,35 +669,6 @@ it3.remove(); // Error: list3 does not have @IteratorPolyMod \end{Verbatim} -% \sectionAndLabel{Unverified collection implementation annotations}{modifiability-class-unverified} -% -% The Modifiability Checker verifies uses of collection modifiability annotations, -% but it does not verify collection implementations. -% -% When a user-defined collection-like class (a \, \, -% \, or subtype) explicitly writes a concrete modifiability qualifier -% on the class declaration or on a constructor, the checker issues a -% \ warning. The warning means that the -% annotation is a trusted specification of the custom implementation, not a fact -% verified from the implementation's method bodies. After examining the -% implementation, suppress the warning with -% \<@SuppressWarnings("class.unverified")>. -% -% For example, the class-level \<@Growable> annotation below triggers -% \. It is up to the programmer to verify that -% the implementation of \ is not \@Growable and remove it from the class declaration. -% \begin{Verbatim} -% public @Growable class TestMod extends ArrayList { -% -% @Override -% public boolean add(@Growable TestMod this, String s) { -% throw new UnsupportedOperationException(); -% } -% } -% \end{Verbatim} - - - \sectionAndLabel{Examples of Modifiability Checker warnings}{modifiability-examples} \begin{Verbatim} diff --git a/framework/tests/all-systems/GenericsCasts.java b/framework/tests/all-systems/GenericsCasts.java index 44b11a13b914..2d9110ff4905 100644 --- a/framework/tests/all-systems/GenericsCasts.java +++ b/framework/tests/all-systems/GenericsCasts.java @@ -27,7 +27,6 @@ void m() { // are currently problematic. // TODO: try to find a problem with skipping this check. class Test { - @SuppressWarnings("class.unverified") @Modifiable class Entry extends LinkedList {} class Queue { diff --git a/framework/tests/all-systems/GenericsEnclosing.java b/framework/tests/all-systems/GenericsEnclosing.java index e5b4bc119448..0c93935ed0e7 100644 --- a/framework/tests/all-systems/GenericsEnclosing.java +++ b/framework/tests/all-systems/GenericsEnclosing.java @@ -9,7 +9,6 @@ * *

    Also see regex/GenericsEnclosing for a test case for the Regex Checker. */ -@SuppressWarnings("class.unverified") public @Growable @Replaceable @SeqUngrowable class GenericsEnclosing extends TreeMap { class Inner { diff --git a/framework/tests/all-systems/RawTypeAssignment.java b/framework/tests/all-systems/RawTypeAssignment.java index 23c3791de84a..b42d16dad10f 100644 --- a/framework/tests/all-systems/RawTypeAssignment.java +++ b/framework/tests/all-systems/RawTypeAssignment.java @@ -4,7 +4,6 @@ class Component {} -@SuppressWarnings("class.unverified") @Modifiable class Components extends ArrayList {} // If we include a type parameter in the superclass, then there is no error below. diff --git a/framework/tests/all-systems/java8inference/Issue3036.java b/framework/tests/all-systems/java8inference/Issue3036.java index e5f7e2f3ef4a..2d074ff3ac3f 100644 --- a/framework/tests/all-systems/java8inference/Issue3036.java +++ b/framework/tests/all-systems/java8inference/Issue3036.java @@ -43,7 +43,6 @@ private void write(Stream stream) { .collect(Collectors.toList()); } - @SuppressWarnings("class.unverified") public static @Modifiable class ImmutableMap extends HashMap { public static ImmutableMap of(K k1, V v1, K k2, V v2) { throw new RuntimeException(); From 9cdd9f8e0d009a3c2e91cfe64c30a9da3c390cdd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 22 Sep 2026 15:50:28 -0700 Subject: [PATCH 295/308] Update linting settings --- .github/workflows/checkbashisms.yaml | 9 +- prek.toml | 129 ++++++++++++--------------- 2 files changed, 62 insertions(+), 76 deletions(-) diff --git a/.github/workflows/checkbashisms.yaml b/.github/workflows/checkbashisms.yaml index 5c1922a4a160..e75debcb8253 100644 --- a/.github/workflows/checkbashisms.yaml +++ b/.github/workflows/checkbashisms.yaml @@ -24,5 +24,10 @@ jobs: # shfmt and shellcheck are run by `prek`. sh_checker_shfmt_disable: true sh_checker_shellcheck_disable: true - # Exclusions. - sh_checker_exclude: gradlew + # Space-separated list of regexes matched against each file name. + # povray is third-party code that is not maintained here. + # checkbashisms cannot check zsh scripts. + sh_checker_exclude: >- + gradlew + tests/kvasir-tests/povray/ + scripts/eps2png-mono-alpha[.]zsh diff --git a/prek.toml b/prek.toml index d2f09c0f716e..d343e0361af4 100644 --- a/prek.toml +++ b/prek.toml @@ -1,28 +1,41 @@ # Configuration file for `prek`, a git hook framework written in Rust. # See https://prek.j178.dev for more information. #:schema https://www.schemastore.org/prek.json +# +# This file is shared across multiple repositories. Not all settings are +# necessarily relevant to the repository where you are reading this comment. + +# In prek's glob matcher: +# * A pattern matches the file's path relative to the repository root. +# * `*` matches `/`, so `dir/*` matches files anywhere beneath `dir`. This file +# uses more conventional (but semantically equivalent) forms: `**` for +# directories, `**/*` for files. +# * A leading `**/` also matches at the root, so `**/foo` matches both `foo` and +# `dots/foo`. (A leading `*/` does not match at the root: `*/foo` matches +# `dots/foo` but not `foo`.) +# * `?` matches a single character. Extglob syntax such as `?(x)` is not +# supported and is interpreted literally. +# * Brace alternation such as `{a,b}` works, but an empty alternative such as +# `{,-*}` does not; spell out every alternative, as in `{ps,ps-*}`. exclude = { glob = [ - "**/*.diff", - "**/*.diff3", - "**/*.goal", - "**/*.goal.*", + "**/*.{diff,diff3}", + "**/*.{goal,goal.*}", "**/*.graffle", "**/*.ics", "**/*.key", "**/*.patch", - "**/*.pdf", - "**/*.pdf-*", - "**/*.ppt", - "**/*.pptx", - "**/*.ps", - "**/*.ps-*", + "**/*.{pdf,pdf-*}", + "**/*.{ppt,pptx}", + "**/*.{ps,ps-*}", "**/*.svg", "**/*expected*", "**/COPYING", - "src/main/resources/specifications/jdk/*", + "gradlew", + "gradlew.bat", + "src/main/resources/specifications/jdk/**/*", "src/test/resources/end-to-end/**/*", - "tests/kvasir-tests/povray/*", + "tests/kvasir-tests/povray/**/*", ] } [[repos]] @@ -31,58 +44,52 @@ hooks = [ { id = "check-added-large-files" }, { id = "check-case-conflict" }, { id = "check-executables-have-shebangs", exclude = { glob = [ - "scripts/*.pl", + "scripts/**/*.pl", "scripts/trace-untruncate", "search", - "tests/kvasir-tests/**", + "tests/kvasir-tests/**/*", ] } }, { id = "check-json" }, { id = "check-merge-conflict" }, { id = "check-shebang-scripts-are-executable" }, { id = "check-symlinks", exclude = { glob = [ - ".subversion/auth", - "dots/.subversion/auth", + "**/.subversion/auth", ] } }, { id = "check-toml" }, { id = "check-vcs-permalinks" }, { id = "check-xml", exclude = { glob = [ - "posts/rss.xml", "blog/feed.xml", + "posts/rss.xml", ] } }, { id = "check-yaml" }, { id = "destroyed-symlinks" }, { id = "detect-private-key" }, { id = "end-of-file-fixer", exclude = { glob = [ "**/.vscode/settings.json", - "**/native-image/**", + "**/native-image/**/*", "**/non-conflicted-file-without-terminator.txt", - "**/tests/squeeze-blank-lines-test/**", - "bugs/jikes-patches/*", - "java/jtb/**", - "tests/kvasir-tests/**", + "**/tests/squeeze-blank-lines-test/**/*", + "bugs/jikes-patches/**/*", + "java/jtb/**/*", + "tests/kvasir-tests/**/*", ] } }, { id = "file-contents-sorter", files = { glob = [ - "src/systemTest/resources/test-covgoals/**", + "src/systemTest/resources/test-covgoals/**/*", ] } }, { id = "fix-byte-order-marker" }, { id = "forbid-new-submodules" }, { id = "mixed-line-ending" }, { id = "trailing-whitespace", exclude = { glob = [ - "**/tests/squeeze-blank-lines-test/**", - ".aspell.en.prepl", - ".aspell.en.pws", + "**/.aspell.en.{prepl,pws}", + "**/en_US_apostrophe.aff", + "**/enscript.st", + "**/tests/squeeze-blank-lines-test/**/*", "bugs/BUGS.to-do", "bugs/bug-mew", - "bugs/jikes-patches/*", - "dots/.aspell.en.prepl", - "dots/.aspell.en.pws", - "dots/en_US_apostrophe.aff", - "dots/enscript.st", + "bugs/jikes-patches/**/*", "emacs/bug/crypted.UUE", - "en_US_apostrophe.aff", - "enscript.st", - "java/jtb/**", - "tests/kvasir-tests/**", + "java/jtb/**/*", + "tests/kvasir-tests/**/*", ] } }, ] @@ -112,11 +119,9 @@ repo = "https://github.com/Lucas-C/pre-commit-hooks-java" rev = "1.3.10" hooks = [ { id = "validate-html", exclude = { glob = [ - "**/bytopic-headfoot.html", - "**/pubs-bytopic-headfoot.html", - "**/pubs-bytopic-underreview-headfoot.html", + "**/*bytopic*headfoot.html", "google04aaa4eb05fd668b.html", - "pubs/*-abstract.html", + "pubs/**/*-abstract.html", "pubs/index.html", "tests/sources/six170/huffman.html", "tools/hierarchical/clustering.html", @@ -147,7 +152,6 @@ hooks = [ "--format={{.FileName}}:{{.LineNumber}}:0: {{.Rule}}: {{.Violation}}\n", ], exclude = { glob = [ "code-style.Makefile", - "tests/kvasir-tests/povray/*", ] } }, ] @@ -169,7 +173,7 @@ hooks = [{ id = "sync-pre-commit-deps" }] [[repos]] repo = "https://github.com/astral-sh/ruff-pre-commit" -rev = "v0.15.22" +rev = "v0.16.6" hooks = [ # Run the linter. { id = "ruff-check", args = ["--fix"], types_or = ["python", "pyi"] }, @@ -218,24 +222,16 @@ hooks = [ "-P", "SCRIPTDIR", ], exclude = { glob = [ - "share/csail-athena-tickets.bash", - "share/addrfilter", - "share/print-mail", - "share/javapp", - "share/myxapps", - "scripts/eps2png-mono-alpha.zsh", - "scripts/browser_emacs", - "scripts/find-dtrace", - "tests/kvasir-tests/tcas/tests.sh", - "tests/kvasir-tests/flex-pristine/mkskel.sh", - "tests/kvasir-tests/tot_info/tests.sh", "cronic-orig", - "classfile_check_version", + "scripts/eps2png-mono-alpha.zsh", + "share/csail-athena-tickets.bash", + "tests/kvasir-tests/**/*", ] } }, ] [[repos]] repo = "https://github.com/scop/pre-commit-shfmt" +# This "version number" is a tag at https://github.com/scop/pre-commit-shfmt/tags . rev = "v3.13.1-1" hooks = [ { id = "shfmt", args = [ @@ -245,27 +241,12 @@ hooks = [ "-bn", "-sr", ], exclude = { glob = [ - "gradlew", - "share/addrfilter", - "dots/conda-initialize.sh", - "conda-initialize.sh", + "**/conda-initialize.sh", ] } }, ] -# TODO: checkbashisms (I need to write my own). -# [[repos]] -# repo = "TODO" -# hooks = [ -# { id = "checkbashisms", types = [ -# "shell", -# ], exclude = { glob = [ -# "share/addrfilter", -# "share/print-mail", -# "share/myxapps", -# "share/javapp", -# "gradlew", -# ] } }, -# ] +# For checkbashisms, use +# https://github.com/mernst/mdedots/blob/main/.github/workflows/checkbashisms.yaml # Problems: # * False positives for E011. @@ -295,7 +276,7 @@ hooks = [ # repo = "https://github.com/crate-ci/typos" # rev = "v1.48.0" # hooks = [ -# { id = "typos", exclude = { glob = [".abbrev_defs", "share/mail-review-spam", "bugs/jikes-patches/*", "share/checkbashisms", "dots/.aspell.en.prepl", ".aspell.en.prepl", "dots/en_US_apostrophe.dic", "dots/.aspell.en.pws", ".aspell.en.pws", "dots/en_US_apostrophe.aff", "en_US_apostrophe.aff" ] } } +# { id = "typos", exclude = { glob = ["**/.abbrev_defs", "share/mail-review-spam.py", "bugs/jikes-patches/**/*", "share/checkbashisms", "**/.aspell.en.prepl", "**/.aspell.en.pws", "**/en_US_apostrophe.aff", "**/en_US_apostrophe.dic" ] } } # ] # Very noisy. Perhaps re-enable later. @@ -303,10 +284,10 @@ hooks = [ # repo = "https://github.com/codespell-project/codespell" # rev = "v2.4.1" # hooks = [ -# { id = "codespell", exclude = { glob = [ "en_US_apostrophe.dic", ".ssh/known_hosts", "enscript.st" ] } }, +# { id = "codespell", exclude = { glob = [ "**/en_US_apostrophe.dic", "**/enscript.st", "**/.ssh/known_hosts" ] } }, # ] -# proselint does not understand fenced code blocks in markdown files. +# proselint does not understand fenced code blocks in Markdown files. # [[repos]] # repo = "https://github.com/amperser/proselint" # rev = "v0.16.0" From 3075622f8f7aa2996ded6b87c1285900f863a5a5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 23 Sep 2026 07:25:36 -0700 Subject: [PATCH 296/308] Fix Modifiability Checker type-variable weakening and polymorphic iterator results * On a type variable or wildcard, `@Modifiable` and `@Unmodifiable` claim a capability only if the upper bound is a subtype of a type known to have it (`typesWithCapability()`). Previously they claimed it unless a type lacking the capability was a subtype of the bound, which missed bounds such as `AbstractCollection` and `Collection & Serializable`. * An iterator method whose declared result is polymorphic keeps the result of polymorphic resolution, whether or not type arguments were inferred. * Other pending fixes to the checker and its tests. --- ...ModifiabilityBaseAnnotatedTypeFactory.java | 102 +++++++++++++---- .../ModifiabilityBaseVisitor.java | 38 +++++-- .../grow/GrowAnnotatedTypeFactory.java | 6 + .../IteratorAnnotatedTypeFactory.java | 8 ++ .../replace/ReplaceAnnotatedTypeFactory.java | 20 ++-- .../seqgrow/SeqGrowAnnotatedTypeFactory.java | 15 +++ .../shrink/ShrinkAnnotatedTypeFactory.java | 6 + .../ConstructorResultTypeTest.java | 4 +- .../InheritedImplementationTest.java | 66 +++++++++++ .../IteratorTypeVariableReceiverTest.java | 35 ++++++ .../tests/modifiability/ListQueueTest.java | 57 ++++++++++ .../modifiability/PolyIteratorResultTest.java | 42 +++++++ .../PreservesModifiabilityTest.java | 10 ++ .../modifiability/TypeVariableAliasTest.java | 103 ++++++++++++++++++ 14 files changed, 470 insertions(+), 42 deletions(-) create mode 100644 checker/tests/modifiability/IteratorTypeVariableReceiverTest.java create mode 100644 checker/tests/modifiability/ListQueueTest.java create mode 100644 checker/tests/modifiability/PolyIteratorResultTest.java create mode 100644 checker/tests/modifiability/TypeVariableAliasTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index c60fd510bf2a..57872c3c7664 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -34,6 +34,12 @@ public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotate /** The {@code @}{@link IteratorPolyMod} qualifier. */ protected final AnnotationMirror ITERATOR_POLY_MOD; + /** The erased {@code java.util.Collection} type. */ + protected final TypeMirror collectionErasure; + + /** The erased {@code java.util.Map} type. */ + protected final TypeMirror mapErasure; + /** The erased {@code java.util.Map.Entry} type. */ protected final TypeMirror mapEntryErasure; @@ -57,6 +63,8 @@ public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotate protected ModifiabilityBaseAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, IteratorPolyMod.class); + this.collectionErasure = erasureOf("java.util.Collection"); + this.mapErasure = erasureOf("java.util.Map"); this.mapEntryErasure = erasureOf("java.util.Map.Entry"); this.iteratorErasure = erasureOf("java.util.Iterator"); this.listIteratorErasure = erasureOf("java.util.ListIterator"); @@ -141,6 +149,44 @@ protected boolean typeLacksCapability(TypeMirror type) { return false; } + /** + * Returns erased types whose every subtype has this checker's capability, unless {@link + * #typeLacksCapability} holds of the subtype. For example, the Replace Checker's result includes + * {@code List}. + * + *

    A type variable or wildcard may be instantiated by any subtype of its upper bound, including + * a subtype that also implements an unrelated interface. For example, a type variable whose upper + * bound is {@code AbstractCollection} or {@code Serializable} may be instantiated by {@code + * HashSet}, which cannot be replaced into. So an alias written on a type variable claims this + * checker's capability only if the upper bound is a subtype of one of these types. + * + * @return erased types whose subtypes have this checker's capability + */ + protected abstract List typesWithCapability(); + + /** + * Returns true if a type variable or wildcard whose upper bound is {@code bound} may be + * instantiated by a type that structurally cannot support this checker's capability; that is, if + * no bound is a subtype of a type in {@link #typesWithCapability}. + * + * @param bound the upper bound of a type variable or wildcard; it may be an intersection type + * @return true if some instantiation of the type variable or wildcard may lack the capability + */ + private boolean someInstantiationLacksCapability(TypeMirror bound) { + List bounds = + bound.getKind() == TypeKind.INTERSECTION + ? ((IntersectionType) bound).getBounds() + : List.of(bound); + for (TypeMirror withCapability : typesWithCapability()) { + for (TypeMirror b : bounds) { + if (TypesUtils.isErasedSubtype(b, withCapability, types)) { + return false; + } + } + } + return true; + } + /** * Returns true if {@code @PolyModifiable} weakens to the top qualifier on {@code type}, rather * than to this checker's polymorphic qualifier. This differs from {@link #typeLacksCapability} @@ -205,20 +251,29 @@ protected boolean expandsModifiabilityAliases() { * weakening is applied. * *

    A type variable or wildcard is classified by its upper bound, so that, for example, {@code - * >} has the same capabilities as {@code Deque}. + * >} has the same capabilities as {@code Deque}. In addition, on a type + * variable or wildcard, {@code @Modifiable} and {@code @Unmodifiable} weaken to the top qualifier + * unless the upper bound is a subtype of a type that has this checker's capability; see {@link + * #typesWithCapability}. For example, {@code @Modifiable T} for an unbounded {@code T} is + * {@code @MaybeReplaceable}, because {@code T} may be {@code Set}. */ @Override public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (expandsModifiabilityAliases()) { TypeMirror bound = tm == null ? null : TypesUtils.upperBound(tm); - if (areSameByClass(annotation, Modifiable.class)) { - return bound != null && lacksCapability(bound, this::typeLacksCapability) - ? topAnnotation() - : positiveCapability(); - } else if (areSameByClass(annotation, Unmodifiable.class)) { - return bound != null && lacksCapability(bound, this::typeLacksCapability) - ? topAnnotation() + if (areSameByClass(annotation, Modifiable.class) + || areSameByClass(annotation, Unmodifiable.class)) { + boolean weaken = + bound != null + && (lacksCapability(bound, this::typeLacksCapability) + || ((tm.getKind() == TypeKind.TYPEVAR || tm.getKind() == TypeKind.WILDCARD) + && someInstantiationLacksCapability(bound))); + if (weaken) { + return topAnnotation(); + } + return areSameByClass(annotation, Modifiable.class) + ? positiveCapability() : negativeCapability(); } else if (areSameByClass(annotation, PolyModifiable.class)) { return bound != null && lacksCapability(bound, this::polyLacksCapability) @@ -317,7 +372,9 @@ protected void refineReturnTypeForPreservesModifiability( return; } AnnotatedTypeMirror argumentType = getAnnotatedType(tree.getArguments().get(0)); - if (argumentType.hasPrimaryAnnotation(positiveCapability())) { + // `hasAnnotation()` rather than `hasPrimaryAnnotation()`, so that an argument whose type is a + // type variable is classified by its upper bound. + if (argumentType.hasAnnotation(positiveCapability())) { returnType.replaceAnnotation(positiveCapability()); } else { returnType.replaceAnnotation(topAnnotation()); @@ -334,8 +391,9 @@ protected void refineReturnTypeForPreservesModifiability( * special treatment is needed for iterator methods. * *

    A declared negative result keeps its declared qualifier, since such an iterator never has - * the capability. Otherwise, the result qualifier is computed from the receiver: the iterator of - * a receiver with this checker's negative qualifier also has that negative qualifier, and the + * the capability. A declared polymorphic result keeps the qualifier that polymorphic resolution + * gives it. Otherwise, the result qualifier is computed from the receiver: the iterator of a + * receiver with this checker's negative qualifier also has that negative qualifier, and the * iterator of a receiver that has both this checker's positive qualifier and * {@code @IteratorPolyMod} has the positive qualifier. In every other case the result is the top * qualifier. @@ -369,10 +427,13 @@ protected void refineIteratorReturnType( if (returnType.hasPrimaryAnnotation(negativeCapability())) { return; } - if (returnType.hasPrimaryAnnotation(polyCapability())) { - // `super.methodFromUse()` resolves the polymorphic qualifier only when it infers type - // arguments; see `GenericAnnotatedTypeFactory.methodFromUsePreSubstitution()`. On the other - // path, leave the polymorphic qualifier for that later resolution. + // Keep a declared polymorphic result, which relates the iterator to the receiver. Test the + // declaration rather than `returnType`: `super.methodFromUse()` has already resolved the + // polymorphic qualifier when it infers type arguments, but not otherwise; see + // `GenericAnnotatedTypeFactory.methodFromUsePreSubstitution()`. + if (getAnnotatedType(methodType.getElement()) + .getReturnType() + .hasPrimaryAnnotation(polyCapability())) { return; } @@ -385,19 +446,22 @@ protected void refineIteratorReturnType( return; } + // The receiver tests below use `hasAnnotation()` rather than `hasPrimaryAnnotation()`, so that + // a + // receiver whose type is a type variable is classified by its upper bound. + // The iterator of a collection that lacks the capability also lacks the capability, even if // the declaration says that the iterator has it (as ArrayList's does). - if (receiverType.hasPrimaryAnnotation(negativeCapability())) { + if (receiverType.hasAnnotation(negativeCapability())) { returnType.replaceAnnotation(negativeCapability()); return; } // The receiver has the capability; its iterator does too if the receiver is @IteratorPolyMod. - if (receiverType.hasPrimaryAnnotation(positiveCapability())) { + if (receiverType.hasAnnotation(positiveCapability())) { AnnotatedTypeMirror iteratorHierarchyType = getTypeFactoryOfSubchecker(IteratorChecker.class).getReceiverType(tree); - if (iteratorHierarchyType != null - && iteratorHierarchyType.hasPrimaryAnnotation(ITERATOR_POLY_MOD)) { + if (iteratorHierarchyType != null && iteratorHierarchyType.hasAnnotation(ITERATOR_POLY_MOD)) { returnType.replaceAnnotation(positiveCapability()); return; } diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 3aeef450b029..4cb36f8cb911 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -14,6 +14,7 @@ import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; @@ -48,8 +49,8 @@ * requiring the body of each method that requires the capability -- either on its own * receiver parameter or from a method that it overrides -- to agree with that qualifier about * whether the method throws {@link UnsupportedOperationException}. - *

  • Requiring a class that claims the capability not to inherit, without overriding, a method - * whose implementation always throws {@link UnsupportedOperationException}. + *
  • Requiring a concrete class that claims the capability not to inherit, without overriding, a + * method whose implementation always throws {@link UnsupportedOperationException}. *
  • Requiring an override to preserve a positive receiver capability of the method it * overrides. * @@ -174,15 +175,17 @@ private void processClassMembers(ClassTree tree) { } /** - * Issues an error if the class claims this checker's capability, but inherits without overriding - * a method whose implementation always throws {@link UnsupportedOperationException}. + * Issues an error if a concrete class claims this checker's capability, but inherits without + * overriding a method whose implementation always throws {@link UnsupportedOperationException}. * *

    The body of an inherited method is usually not available -- it is compiled separately, and * the checker sees only its signature -- so the implementation is known to throw only if it is * annotated {@code @}{@link ThrowsUnsupportedOperation}, as {@code AbstractList.set()} is, or if - * it is declared in a class whose constructors declare the negative qualifier, in which case + * it is declared in a class whose constructors all declare the negative qualifier, in which case * {@link #checkImplOK} verified that it throws. * + *

    An abstract class is not checked, because its concrete subclasses are. + * * @param tree a class * @param classElement the element for {@code tree} * @param constructorAnno the result qualifier that the class's constructors declare @@ -197,6 +200,11 @@ private void checkInheritedImplementations( // UnsupportedOperationException on account of this checker's capability.) return; } + if (classElement.getModifiers().contains(Modifier.ABSTRACT)) { + // No value's run-time class is an abstract class, and each concrete subclass is checked on + // its own. A concrete subclass may override the inherited method. + return; + } for (ExecutableElement method : ElementFilter.methodsIn(elements.getAllMembers(classElement))) { TypeElement declaringClass = ElementUtils.enclosingTypeElement(method); if (declaringClass == null || declaringClass.equals(classElement)) { @@ -232,10 +240,12 @@ private boolean implementationThrowsUOE(ExecutableElement method, TypeElement de if (atypeFactory.getDeclAnnotation(method, ThrowsUnsupportedOperation.class) != null) { return true; } - // A class whose constructors declare the negative qualifier, such as @Ungrowable, was itself - // checked: every method of it that requires the capability throws - // UnsupportedOperationException. A class that declares nothing, such as AbstractList, says - // nothing about its methods, and only the @ThrowsUnsupportedOperation annotation does. + // A class whose constructors all declare the negative qualifier, such as @Ungrowable, was + // itself checked: every method of it that requires the capability throws + // UnsupportedOperationException. A class whose constructors disagree was not checked (see + // processClassMembers), and a class that declares nothing, such as AbstractList, says nothing + // about its methods; for them, only the @ThrowsUnsupportedOperation annotation does. + boolean sawNegative = false; for (ExecutableElement constructor : ElementFilter.constructorsIn(declaringClass.getEnclosedElements())) { AnnotationMirror resultAnno = @@ -243,11 +253,15 @@ private boolean implementationThrowsUOE(ExecutableElement method, TypeElement de .getAnnotatedType(constructor) .getReturnType() .getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); - if (resultAnno != null && isNegativeCapability(resultAnno)) { - return true; + if (resultAnno == null) { + continue; + } + if (!isNegativeCapability(resultAnno)) { + return false; } + sawNegative = true; } - return false; + return sawNegative; } /** diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java index 9b82c8a1785a..6334aac42510 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; @@ -93,6 +94,11 @@ protected boolean typeLacksCapability(TypeMirror type) { && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } + @Override + protected List typesWithCapability() { + return List.of(collectionErasure, mapErasure, listIteratorErasure); + } + /** * For {@code Map.Entry}, only the replace bit is meaningful to carry from the map receiver, so * the grow bit of {@code @PolyModifiable} is {@code @MaybeGrowable}. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java index a1b49585f3e8..30d8e05028b7 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -3,8 +3,10 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.modifiability.ModifiabilityBaseAnnotatedTypeFactory; import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.MaybeIteratorPolyMod; @@ -65,6 +67,12 @@ protected boolean expandsModifiabilityAliases() { return false; } + @Override + protected List typesWithCapability() { + // Never called, because the whole-modifiability aliases do not expand into this hierarchy. + return List.of(); + } + @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java index 19dccf4df0f1..602d0cef1ee1 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; @@ -24,14 +25,11 @@ public class ReplaceAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeF /** The erased {@code java.util.Set} type. */ private final TypeMirror setErasure; - /** The erased {@code java.util.Collection} type. */ - private final TypeMirror collectionErasure; - /** The erased {@code java.util.Queue} type. */ private final TypeMirror queueErasure; - /** The erased {@code java.util.LinkedList} type. */ - private final TypeMirror linkedListErasure; + /** The erased {@code java.util.List} type. */ + private final TypeMirror listErasure; /** The {@code @}{@link MaybeReplaceable} qualifier. */ private final AnnotationMirror MAYBE_REPLACEABLE; @@ -54,9 +52,8 @@ public class ReplaceAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeF public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.setErasure = erasureOf("java.util.Set"); - this.collectionErasure = erasureOf("java.util.Collection"); this.queueErasure = erasureOf("java.util.Queue"); - this.linkedListErasure = erasureOf("java.util.LinkedList"); + this.listErasure = erasureOf("java.util.List"); this.MAYBE_REPLACEABLE = AnnotationBuilder.fromClass(elements, MaybeReplaceable.class); this.REPLACEABLE = AnnotationBuilder.fromClass(elements, Replaceable.class); this.UNREPLACEABLE = AnnotationBuilder.fromClass(elements, Unreplaceable.class); @@ -96,7 +93,7 @@ protected Set> createSupportedTypeQualifiers() { } /** - * An exact {@code Collection}, a {@code Set}, a non-{@code LinkedList} {@code Queue}, and a + * An exact {@code Collection}, a {@code Set}, a {@code Queue} that is not a {@code List}, and a * non-{@code ListIterator} {@code Iterator} have no replace methods, so {@code @Modifiable} and * {@code @Unmodifiable} make no claim about them. */ @@ -108,11 +105,16 @@ protected boolean typeLacksCapability(TypeMirror type) { return types.isSameType(types.erasure(type), collectionErasure) || TypesUtils.isErasedSubtype(type, setErasure, types) || (TypesUtils.isErasedSubtype(type, queueErasure, types) - && !TypesUtils.isErasedSubtype(type, linkedListErasure, types)) + && !TypesUtils.isErasedSubtype(type, listErasure, types)) || (TypesUtils.isErasedSubtype(type, iteratorErasure, types) && !TypesUtils.isErasedSubtype(type, listIteratorErasure, types)); } + @Override + protected List typesWithCapability() { + return List.of(listErasure, listIteratorErasure, mapErasure, mapEntryErasure); + } + // polyLacksCapability is not overridden: unlike grow and shrink for Map.Entry, replacement // through Entry.setValue is a meaningful capability to carry from the map receiver. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java index 11e7897404ed..bde38f661de4 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -1,8 +1,10 @@ package org.checkerframework.checker.modifiability.seqgrow; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.TypeElement; @@ -129,6 +131,19 @@ protected boolean typeLacksCapability(TypeMirror type) { return !canSeqGrow; } + @Override + protected List typesWithCapability() { + List result = new ArrayList<>(3); + if (sequencedCollectionErasure != null) { + result.add(sequencedCollectionErasure); + } + if (sequencedMapErasure != null) { + result.add(sequencedMapErasure); + } + result.add(dequeErasure); + return result; + } + /** * For {@code Map.Entry}, only the replace bit is meaningful to carry from the map receiver, so * the sequenced-grow bit of {@code @PolyModifiable} is {@code @MaybeSeqGrowable}. diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java index be9c37aeba46..780383b63072 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; @@ -91,6 +92,11 @@ protected boolean typeLacksCapability(TypeMirror type) { return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); } + @Override + protected List typesWithCapability() { + return List.of(collectionErasure, mapErasure, iteratorErasure); + } + /** * For {@code Map.Entry}, only the replace bit is meaningful to carry from the map receiver, so * the shrink bit of {@code @PolyModifiable} is {@code @MaybeShrinkable}. diff --git a/checker/tests/modifiability/ConstructorResultTypeTest.java b/checker/tests/modifiability/ConstructorResultTypeTest.java index 94c7dbd94fdb..34f749eaacbb 100644 --- a/checker/tests/modifiability/ConstructorResultTypeTest.java +++ b/checker/tests/modifiability/ConstructorResultTypeTest.java @@ -1,7 +1,8 @@ // Tests the `inconsistent.constructor.result.type` and `bottom.annotation.on.receiver` checks, // which are performed by ModifiabilityBaseVisitor.processClassTree. // -// A class below whose constructors are @Growable also gets an `inherited.implementation.uoe` +// A concrete class below whose constructors are @Growable also gets an +// `inherited.implementation.uoe` // error, because it extends AbstractList without overriding `add(int, E)`, whose implementation // throws UnsupportedOperationException. See InheritedImplementationTest.java. @@ -98,7 +99,6 @@ public void bottomReceiver(@BottomGrowable InconsistentConstructorsAndBottomRece } // The method has no body, but the bottom qualifier on its receiver is still an error. - // :: error: [inherited.implementation.uoe] abstract static class AbstractBottomReceiver extends AbstractList { @Growable AbstractBottomReceiver() {} diff --git a/checker/tests/modifiability/InheritedImplementationTest.java b/checker/tests/modifiability/InheritedImplementationTest.java index 1cddf28eed6b..762dfd6ce879 100644 --- a/checker/tests/modifiability/InheritedImplementationTest.java +++ b/checker/tests/modifiability/InheritedImplementationTest.java @@ -66,6 +66,72 @@ public void growThrows(@Growable GrowableSubclassThatOverrides this) {} public void add(@Growable GrowableSubclassThatOverrides this, int index, String element) {} } + // An abstract class is not checked for inherited implementations; its concrete subclasses are. + abstract static class AbstractGrowableBase extends AbstractList { + @Growable AbstractGrowableBase() {} + } + + static class ConcreteSubclassThatOverrides extends AbstractGrowableBase { + @Growable ConcreteSubclassThatOverrides() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + @Override + public void add(@Growable ConcreteSubclassThatOverrides this, int index, String element) {} + } + + // :: error: [inherited.implementation.uoe] + static class ConcreteSubclassThatDoesNotOverride extends AbstractGrowableBase { + @Growable ConcreteSubclassThatDoesNotOverride() {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + } + + // The constructors disagree, so the method bodies of this class were not checked, and nothing is + // known about whether `doesNotThrow()` throws. (It does not.) + static class InconsistentConstructors extends AbstractList { + @Ungrowable InconsistentConstructors() {} + + // :: error: [inconsistent.constructor.result.type] + @Growable InconsistentConstructors(int capacity) {} + + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 0; + } + + public void doesNotThrow(@Growable InconsistentConstructors this) {} + + @Override + public void add(@Growable InconsistentConstructors this, int index, String element) {} + } + + // The subclass inherits `doesNotThrow()`, which is not known to throw. + static class GrowableSubclassOfInconsistent extends InconsistentConstructors { + @Growable GrowableSubclassOfInconsistent() {} + } + /** A subclass of UnsupportedOperationException is an UnsupportedOperationException. */ static class MyUnsupportedOperationException extends UnsupportedOperationException {} diff --git a/checker/tests/modifiability/IteratorTypeVariableReceiverTest.java b/checker/tests/modifiability/IteratorTypeVariableReceiverTest.java new file mode 100644 index 000000000000..7bbe521c771a --- /dev/null +++ b/checker/tests/modifiability/IteratorTypeVariableReceiverTest.java @@ -0,0 +1,35 @@ +import java.util.Iterator; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; + +/** + * The result of an iterator method whose receiver is a type variable is refined from the upper + * bound of the type variable, just as it would be from a receiver of the bound's type. + */ +public class IteratorTypeVariableReceiverTest { + + > void positive(L list) { + Iterator it = list.iterator(); + it.remove(); + @Shrinkable Iterator shrinkable = list.iterator(); + } + + void positiveDeclared(@Shrinkable @IteratorPolyMod List list) { + Iterator it = list.iterator(); + it.remove(); + } + + > void negative(L list) { + @Unshrinkable Iterator it = list.iterator(); + } + + // The receiver can shrink, but it does not promise that its iterator can. + > void notIteratorPolyMod(L list) { + @MaybeShrinkable Iterator it = list.iterator(); + // :: error: [assignment] + @Shrinkable Iterator shrinkable = list.iterator(); + } +} diff --git a/checker/tests/modifiability/ListQueueTest.java b/checker/tests/modifiability/ListQueueTest.java new file mode 100644 index 000000000000..a267df993300 --- /dev/null +++ b/checker/tests/modifiability/ListQueueTest.java @@ -0,0 +1,57 @@ +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Queue; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; + +/** + * A {@code Queue} has no replace methods, so {@code @Modifiable} makes no claim about replacing + * into it -- unless it is also a {@code List}, which does have replace methods. + */ +public class ListQueueTest { + + /** A list that is also a queue, like {@code LinkedList}. */ + static class ListQueue extends ArrayList implements Queue { + @Override + public boolean offer(@Growable ListQueue this, E e) { + return add(e); + } + + @Override + public E remove(@Shrinkable ListQueue this) { + return remove(0); + } + + @Override + public E poll(@Shrinkable ListQueue this) { + return isEmpty() ? null : remove(0); + } + + @Override + public E element() { + return get(0); + } + + @Override + public E peek() { + return isEmpty() ? null : get(0); + } + } + + void userListQueue(@Modifiable ListQueue q) { + @Replaceable ListQueue replaceable = q; + } + + void linkedList(@Modifiable LinkedList q) { + @Replaceable LinkedList replaceable = q; + } + + void plainQueue(@Modifiable Queue q) { + @MaybeReplaceable Queue maybe = q; + // :: error: [assignment] + @Replaceable Queue replaceable = q; + } +} diff --git a/checker/tests/modifiability/PolyIteratorResultTest.java b/checker/tests/modifiability/PolyIteratorResultTest.java new file mode 100644 index 000000000000..4862bf87ace2 --- /dev/null +++ b/checker/tests/modifiability/PolyIteratorResultTest.java @@ -0,0 +1,42 @@ +import java.util.AbstractCollection; +import java.util.Iterator; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; +import org.checkerframework.checker.modifiability.qual.PolyShrinkable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; + +/** + * An iterator method whose result is declared polymorphic relates the iterator to the receiver, so + * its result is the qualifier that polymorphic resolution gives it, even though the receiver is not + * {@code @IteratorPolyMod}. + */ +public class PolyIteratorResultTest extends AbstractCollection { + + // `AbstractCollection.iterator()` declares its receiver `@MaybeModifiable`, so a polymorphic + // receiver is a stronger requirement, but the result must still be resolved polymorphically. + @Override + // :: error: [override.receiver] + public @PolyShrinkable Iterator iterator(@PolyShrinkable PolyIteratorResultTest this) { + throw new Error("not implemented"); + } + + @Override + public int size() { + return 1; + } + + void shrinkableReceiver(@Shrinkable PolyIteratorResultTest c) { + @Shrinkable Iterator it = c.iterator(); + it.remove(); + } + + void unshrinkableReceiver(@Unshrinkable PolyIteratorResultTest c) { + @Unshrinkable Iterator it = c.iterator(); + } + + void unknownReceiver(@MaybeShrinkable PolyIteratorResultTest c) { + @MaybeShrinkable Iterator it = c.iterator(); + // :: error: [assignment] + @Shrinkable Iterator shrinkable = c.iterator(); + } +} diff --git a/checker/tests/modifiability/PreservesModifiabilityTest.java b/checker/tests/modifiability/PreservesModifiabilityTest.java index 33bbad2bb9cb..640721bd6231 100644 --- a/checker/tests/modifiability/PreservesModifiabilityTest.java +++ b/checker/tests/modifiability/PreservesModifiabilityTest.java @@ -77,6 +77,16 @@ void unannotatedDoesNotPreserve( @Modifiable List m = unannotated(modifiable); } + // An argument whose type is a type variable is classified by the type variable's upper bound. + > void typeVariableArgument(C growable) { + @Growable List g = annotated(growable); + } + + > void typeVariableArgumentWithoutCapability(C unknown) { + // :: error: [assignment] + @Growable List g = annotated(unknown); + } + // Despite the error on its declaration, the annotation has no effect on a call to such a method. void misplacedAnnotationHasNoEffect(@Growable List growable) { annotatedVoid(growable); diff --git a/checker/tests/modifiability/TypeVariableAliasTest.java b/checker/tests/modifiability/TypeVariableAliasTest.java new file mode 100644 index 000000000000..029ada06e036 --- /dev/null +++ b/checker/tests/modifiability/TypeVariableAliasTest.java @@ -0,0 +1,103 @@ +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.MaybeGrowable; +import org.checkerframework.checker.modifiability.qual.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.MaybeShrinkable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; +import org.checkerframework.checker.modifiability.qual.Unreplaceable; + +/** + * A type variable may be instantiated by any subtype of its upper bound, including a subtype that + * also implements an unrelated interface. Unless the upper bound is a subtype of a type that has a + * capability, such as {@code List} for replacement or {@code Collection} for growth, + * {@code @Modifiable} and {@code @Unmodifiable} on the type variable make no claim about that + * capability. + */ +public class TypeVariableAliasTest { + + static void takeModifiable(@Modifiable T t) {} + + static void takeUnmodifiable(@Unmodifiable T t) {} + + static > void takeModifiableCollection(@Modifiable T t) {} + + static > void takeModifiableList(@Modifiable T t) {} + + static > void takeUnmodifiableList(@Unmodifiable T t) {} + + void unboundedTypeVariable( + @Modifiable Set modifiableSet, + @Unmodifiable Set unmodifiableSet, + Map.@Modifiable Entry modifiableEntry, + @Modifiable List modifiableList) { + takeModifiable(modifiableSet); + takeUnmodifiable(unmodifiableSet); + takeModifiable(modifiableEntry); + takeModifiable(modifiableList); + } + + void collectionBound(@Modifiable Set modifiableSet) { + takeModifiableCollection(modifiableSet); + } + + void unboundedMeaning(@Modifiable T t) { + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable T canonical = t; + // :: error: [assignment] + @Growable T growable = t; + } + + > void collectionBoundMeaning(@Modifiable T t) { + // `Collection` has no subtype that cannot grow or shrink, but `Set` cannot be replaced into. + @Growable @Shrinkable @MaybeReplaceable T canonical = t; + // :: error: [assignment] + @Replaceable T replaceable = t; + } + + // No subtype of `List` lacks any of these capabilities, so the aliases keep their full meaning. + > void listBoundMeaning(@Modifiable T t, @Unmodifiable T u) { + @Growable @Shrinkable @Replaceable T canonical = t; + @Unreplaceable T unreplaceable = u; + } + + void listBound( + @Modifiable ArrayList modifiable, @Unmodifiable List unmodifiable) { + takeModifiableList(modifiable); + takeUnmodifiableList(unmodifiable); + // :: error: [argument] + takeModifiableList(unmodifiable); + } + + static > void takeModifiableAbstractCollection( + @Modifiable T t) {} + + static & Serializable> void takeModifiableSerializableCollection( + @Modifiable T t) {} + + // `HashSet` is a subtype of both bounds, though `Set` is a subtype of neither. + void unrelatedBound(@Modifiable HashSet modifiableSet) { + takeModifiableAbstractCollection(modifiableSet); + takeModifiableSerializableCollection(modifiableSet); + } + + > void abstractCollectionBoundMeaning(@Modifiable T t) { + @Growable @Shrinkable @MaybeReplaceable T canonical = t; + // :: error: [assignment] + @Replaceable T replaceable = t; + } + + & Serializable> void intersectionBoundMeaning(@Modifiable T t) { + @Growable @Shrinkable @MaybeReplaceable T canonical = t; + // :: error: [assignment] + @Replaceable T replaceable = t; + } +} From 332cd4194700fc3550fde2cd1e0dcb52cc46a26f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 23 Sep 2026 08:31:48 -0700 Subject: [PATCH 297/308] Suppress nullness warning --- .../modifiability/ModifiabilityBaseAnnotatedTypeFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 57872c3c7664..847a491643e1 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -266,6 +266,8 @@ public AnnotationMirror canonicalAnnotation( || areSameByClass(annotation, Unmodifiable.class)) { boolean weaken = bound != null + // `tm != null` is redundant because if `bound` is non-null, then so is `tm`. + && tm != null && (lacksCapability(bound, this::typeLacksCapability) || ((tm.getKind() == TypeKind.TYPEVAR || tm.getKind() == TypeKind.WILDCARD) && someInstantiationLacksCapability(bound))); From e68d99627399af6cf60e7409d92c705841a0fabd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 23 Sep 2026 21:37:41 -0700 Subject: [PATCH 298/308] Add test that reproduces a StackOverflowError for a poly-expression argument to a @PreservesModifiability method --- ...reservesModifiabilityPolyArgumentTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java diff --git a/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java b/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java new file mode 100644 index 000000000000..8279dafa0606 --- /dev/null +++ b/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java @@ -0,0 +1,20 @@ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.modifiability.qual.PreservesModifiability; + +// The argument to a generic `@PreservesModifiability` method is itself a generic method invocation, +// whose type arguments are inferred from the enclosing invocation. Computing the type of the +// argument requires the type of the enclosing invocation, which requires the type of the argument. +class PreservesModifiabilityPolyArgumentTest { + + @PreservesModifiability + static List withoutDuplicates(Collection values) { + return new ArrayList<>(values); + } + + void use() { + withoutDuplicates(Arrays.asList(2, 3, 1)); + } +} From 43296b275177d8b990890e67fc34de841dd72a36 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Thu, 24 Sep 2026 08:25:04 -0700 Subject: [PATCH 299/308] Fix StackOverflowError for a poly-expression argument to a @PreservesModifiability method (#11) --- ...ModifiabilityBaseAnnotatedTypeFactory.java | 5 +++- ...reservesModifiabilityPolyArgumentTest.java | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 847a491643e1..44561beaa11e 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -322,8 +322,11 @@ protected ParameterizedExecutableType methodFromUse( refineIteratorReturnType(tree, method); } + // When `inferTypeArgs` is false, this is being called during type argument inference, possibly + // while inferring the type of the argument itself. Refining would compute the argument's type, + // which would re-enter inference for the argument and recurse infinitely. ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); - if (getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + if (inferTypeArgs && getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { refineReturnTypeForPreservesModifiability(tree, method); } diff --git a/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java b/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java index 8279dafa0606..5a188a999ff9 100644 --- a/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java +++ b/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java @@ -2,7 +2,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.PreservesModifiability; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; // The argument to a generic `@PreservesModifiability` method is itself a generic method invocation, // whose type arguments are inferred from the enclosing invocation. Computing the type of the @@ -14,7 +18,33 @@ static List withoutDuplicates(Collection values) { return new ArrayList<>(values); } + static @Growable List makeGrowable() { + throw new Error(); + } + + static List makeUnknown() { + throw new Error(); + } + void use() { withoutDuplicates(Arrays.asList(2, 3, 1)); } + + // `Arrays.asList` returns a `@Replaceable @IteratorPolyMod` list. + void preservesAsList() { + @Replaceable List r = withoutDuplicates(Arrays.asList(2, 3, 1)); + @IteratorPolyMod List i = withoutDuplicates(Arrays.asList(2, 3, 1)); + // :: error: [assignment] + @Growable List g = withoutDuplicates(Arrays.asList(2, 3, 1)); + // :: error: [assignment] + @Shrinkable List s = withoutDuplicates(Arrays.asList(2, 3, 1)); + } + + void preservesGenericArgument() { + @Growable List g = withoutDuplicates(makeGrowable()); + // :: error: [assignment] + @Shrinkable List s = withoutDuplicates(makeGrowable()); + // :: error: [assignment] + @Growable List u = withoutDuplicates(makeUnknown()); + } } From 04e6d999f92ccdee1ff42629e964932d369084c3 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 29 Sep 2026 22:25:47 -0700 Subject: [PATCH 300/308] Use \hyperref instead of removed \ahrefloc macro --- docs/manual/advanced-features.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 7ad952b8f56c..959a3d6f7fc4 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -1006,7 +1006,7 @@ whether an expression's value can be known at compile time (see \chapterpageref{constant-value-checker}) \item - \ahrefloc{modifiability-checker}{Modifiability Checker} to + \hyperref[modifiability-checker]{Modifiability Checker} to warn about a possible \ at run time when modifying a collection (see \chapterpageref{modifiability-checker}) From 287e2fb3b0bb40579d8f5f12adc82aec657e3e22 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Tue, 29 Sep 2026 22:48:06 -0700 Subject: [PATCH 301/308] fix the comments --- .../ModifiabilityBaseChecker.java | 5 +++- .../modifiability/ModifiabilityVisitor.java | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java index 782e2e0c2b84..6b3fe06e12a5 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -10,7 +10,9 @@ /** * Base class for the Modifiability sub-checkers. * - *

    This class exists so that the sub-checkers share two things: + *

    The sub-checkers' shared type-checking logic is in {@link + * ModifiabilityBaseAnnotatedTypeFactory} and {@link ModifiabilityBaseVisitor}. In addition, this + * class lets the sub-checkers share: * *

      *
    • The {@code messages.properties} file in {@code org.checkerframework.checker.modifiability}, @@ -20,6 +22,7 @@ * keys would need to be duplicated in each sub-checker's package. *
    • The stub files in {@code org.checkerframework.checker.modifiability}; see {@link * #getExtraStubFiles}. + *
    • The Iterator Checker as a subchecker; see {@link #usesIteratorChecker}. *
    */ public abstract class ModifiabilityBaseChecker extends BaseTypeChecker { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java index 08f0665cfcc7..ec103e423b58 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -13,7 +13,13 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; -/** Visitor for the aggregate ModifiabilityChecker. */ +/** + * Visitor for the aggregate ModifiabilityChecker. + * + *

    The checks in this class are in the aggregate checker rather than in {@link + * ModifiabilityBaseVisitor} because they do not depend on a modifiability hierarchy; running them + * in each sub-checker would issue the same error five times. + */ public class ModifiabilityVisitor extends SourceVisitor { /** Fully-qualified name for {@link UnmodifiableParam}. */ @@ -95,10 +101,6 @@ public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) { * without overriding it does not support the operation. A method with no body, such as an * abstract method, has no implementation to make the promise about, so it is an error too. * - *

    This check is in the aggregate checker rather than in {@link ModifiabilityBaseVisitor} - * because it does not depend on a modifiability hierarchy; running it in each sub-checker would - * issue the same error five times. - * * @param tree a method declaration */ private void checkThrowsUnsupportedOperation(MethodTree tree) { @@ -116,12 +118,12 @@ private void checkThrowsUnsupportedOperation(MethodTree tree) { * method that has exactly one formal parameter, which is not a varargs parameter, and a non-void * result. * - *

    The annotation relates the method's result to its first argument, so it says nothing about - * such a method. Worse, on a method with more than one formal parameter it would silently use the - * first argument, which need not be the one that the programmer had in mind. A varargs method is - * rejected for the same reason: a call's first argument is an element of the varargs array rather - * than the sole formal parameter, so the annotation would relate the result to a value of a - * different type. + *

    These restrictions follow from what the annotation means: the method preserves modifiability + * capabilities from its argument to its result. That requires a result and exactly one argument. + * With more than one formal parameter, it would be ambiguous which argument the result depends + * on. A varargs parameter is excluded because a call's argument may be an element of the varargs + * array rather than the array itself, and that element has a different type than the formal + * parameter. * * @param tree a method declaration */ From 191b9cb48abe68f0f728517379be79ebb14b6cf1 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 00:23:59 -0700 Subject: [PATCH 302/308] use processMethodTree to handle constructor and inline atypeFactory.positiveCapability. --- .../ModifiabilityBaseVisitor.java | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 4cb36f8cb911..7dd48155f732 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -94,9 +94,17 @@ public void processClassTree(ClassTree tree) { processClassMembers(tree); } + @Override + public void processMethodTree(String className, MethodTree tree) { + super.processMethodTree(className, tree); + if (!TreeUtils.isConstructor(tree)) { + checkReceiverNotBottom(tree); + } + } + /** - * Processes the members of a class: checks that no method writes the bottom qualifier on its - * receiver, and checks the constructors against one another and against the method bodies. + * Processes the members of a class: checks the constructors against one another and against the + * method bodies. * * @param tree a class */ @@ -123,12 +131,6 @@ private void processClassMembers(ClassTree tree) { } } - // Writing the bottom qualifier on a receiver is an error no matter what the class's - // constructors say, so this check is done before, and independently of, the checks below. - for (MethodTree method : methods) { - checkReceiverNotBottom(method); - } - boolean thisClassWarned = false; AnnotationMirror constructorAnno = null; @@ -193,7 +195,7 @@ private void processClassMembers(ClassTree tree) { private void checkInheritedImplementations( ClassTree tree, TypeElement classElement, AnnotationMirror constructorAnno) { if (!atypeFactory.hasNegativeCapability() - || !AnnotationUtils.areSameByName(constructorAnno, positiveCapability())) { + || !AnnotationUtils.areSameByName(constructorAnno, atypeFactory.positiveCapability())) { // The class does not claim the capability, so an inherited implementation that throws // UnsupportedOperationException agrees with what the class says about itself. (In the // Iterator hierarchy, which has no negative qualifier, no implementation throws @@ -215,7 +217,8 @@ private void checkInheritedImplementations( continue; } AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); - if (receiverType == null || !receiverType.hasPrimaryAnnotation(positiveCapability())) { + if (receiverType == null + || !receiverType.hasPrimaryAnnotation(atypeFactory.positiveCapability())) { continue; } checker.reportError( @@ -335,8 +338,8 @@ private void checkReceiverNotBottom(MethodTree method) { // nothing, and a negative or bottom qualifier is not a requirement that this method must // live up to. if (overriddenReceiver != null - && overriddenReceiver.hasPrimaryAnnotation(positiveCapability())) { - return positiveCapability(); + && overriddenReceiver.hasPrimaryAnnotation(atypeFactory.positiveCapability())) { + return atypeFactory.positiveCapability(); } } return null; @@ -351,7 +354,7 @@ private void checkReceiverNotBottom(MethodTree method) { */ private void checkImplOK( MethodTree method, AnnotationMirror receiverAnno, AnnotationMirror constructorAnno) { - if (!AnnotationUtils.areSameByName(receiverAnno, positiveCapability())) { + if (!AnnotationUtils.areSameByName(receiverAnno, atypeFactory.positiveCapability())) { // The receiver does not require the capability -- it is the top, polymorphic, negative, or // bottom qualifier -- so the method body is unconstrained. (Writing the bottom qualifier on // a receiver is diagnosed by checkReceiverNotBottom.) @@ -369,7 +372,7 @@ private void checkImplOK( if (!implIsUOE(method, types, elements)) { checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); } - } else if (AnnotationUtils.areSameByName(constructorAnno, positiveCapability())) { + } else if (AnnotationUtils.areSameByName(constructorAnno, atypeFactory.positiveCapability())) { if (implIsUOE(method, types, elements)) { checker.reportError(method, "method.implementation.is.uoe", constructorAnnoName); } @@ -428,16 +431,6 @@ static boolean implIsUOE(MethodTree method, Types types, Elements elements) { types.erasure(TreeUtils.typeOf(exception)), types.erasure(unsupportedOperationException)); } - /** - * Returns the positive qualifier for this checker's modifiability hierarchy, such as - * {@code @Growable}, {@code @SeqGrowable}, {@code @Shrinkable}, or {@code @Replaceable}. - * - * @return this checker's positive capability qualifier - */ - private AnnotationMirror positiveCapability() { - return atypeFactory.positiveCapability(); - } - /** * Checks the normal override rules, then requires overrides to preserve any positive * modifiability receiver capability from the overridden method. @@ -490,10 +483,8 @@ protected boolean checkOverride( return true; } - AnnotationMirror positiveCapability = positiveCapability(); - - if (overriddenReceiver.hasPrimaryAnnotation(positiveCapability) - && !overriderReceiver.hasPrimaryAnnotation(positiveCapability)) { + if (overriddenReceiver.hasPrimaryAnnotation(atypeFactory.positiveCapability()) + && !overriderReceiver.hasPrimaryAnnotation(atypeFactory.positiveCapability())) { // Use FoundRequired, as the framework's own `override.receiver` report does, so that the // message key is rendered the same way no matter which check produced it. FoundRequired pair = FoundRequired.of(overriderReceiver, overriddenReceiver); From fd85daaaca77ad347cea2402ff06cec1692d4293 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 09:31:55 -0700 Subject: [PATCH 303/308] use ErasedSubtype and clean up --- ...ModifiabilityBaseAnnotatedTypeFactory.java | 64 +++++++++++++------ .../ModifiabilityBaseVisitor.java | 31 ++++----- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index 44561beaa11e..f4ea0dce40f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -31,6 +31,26 @@ /** Shared annotated type factory logic for the Modifiability sub-checkers. */ public abstract class ModifiabilityBaseAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + /** Fully-qualified class name of {@link Modifiable}. */ + private static final String MODIFIABLE_NAME = + "org.checkerframework.checker.modifiability.qual.Modifiable"; + + /** Fully-qualified class name of {@link Unmodifiable}. */ + private static final String UNMODIFIABLE_NAME = + "org.checkerframework.checker.modifiability.qual.Unmodifiable"; + + /** Fully-qualified class name of {@link PolyModifiable}. */ + private static final String POLY_MODIFIABLE_NAME = + "org.checkerframework.checker.modifiability.qual.PolyModifiable"; + + /** Fully-qualified class name of {@link MaybeModifiable}. */ + private static final String MAYBE_MODIFIABLE_NAME = + "org.checkerframework.checker.modifiability.qual.MaybeModifiable"; + + /** Fully-qualified class name of {@link UnmodifiableParam}. */ + private static final String UNMODIFIABLE_PARAM_NAME = + "org.checkerframework.checker.modifiability.qual.UnmodifiableParam"; + /** The {@code @}{@link IteratorPolyMod} qualifier. */ protected final AnnotationMirror ITERATOR_POLY_MOD; @@ -262,28 +282,32 @@ public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (expandsModifiabilityAliases()) { TypeMirror bound = tm == null ? null : TypesUtils.upperBound(tm); - if (areSameByClass(annotation, Modifiable.class) - || areSameByClass(annotation, Unmodifiable.class)) { - boolean weaken = - bound != null - // `tm != null` is redundant because if `bound` is non-null, then so is `tm`. - && tm != null - && (lacksCapability(bound, this::typeLacksCapability) - || ((tm.getKind() == TypeKind.TYPEVAR || tm.getKind() == TypeKind.WILDCARD) - && someInstantiationLacksCapability(bound))); - if (weaken) { + String name = AnnotationUtils.annotationName(annotation); + switch (name) { + case MODIFIABLE_NAME, UNMODIFIABLE_NAME -> { + boolean weaken = + bound != null + // `tm != null` is redundant because if `bound` is non-null, then so is `tm`. + && tm != null + && (lacksCapability(bound, this::typeLacksCapability) + || ((tm.getKind() == TypeKind.TYPEVAR || tm.getKind() == TypeKind.WILDCARD) + && someInstantiationLacksCapability(bound))); + if (weaken) { + return topAnnotation(); + } + return name.equals(MODIFIABLE_NAME) ? positiveCapability() : negativeCapability(); + } + case POLY_MODIFIABLE_NAME -> { + return bound != null && lacksCapability(bound, this::polyLacksCapability) + ? topAnnotation() + : polyCapability(); + } + case MAYBE_MODIFIABLE_NAME, UNMODIFIABLE_PARAM_NAME -> { return topAnnotation(); } - return areSameByClass(annotation, Modifiable.class) - ? positiveCapability() - : negativeCapability(); - } else if (areSameByClass(annotation, PolyModifiable.class)) { - return bound != null && lacksCapability(bound, this::polyLacksCapability) - ? topAnnotation() - : polyCapability(); - } else if (areSameByClass(annotation, MaybeModifiable.class) - || areSameByClass(annotation, UnmodifiableParam.class)) { - return topAnnotation(); + default -> { + // Not a whole-modifiability alias. + } } } return super.canonicalAnnotation(annotation); diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java index 7dd48155f732..f4274a460062 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -274,35 +274,32 @@ private boolean implementationThrowsUOE(ExecutableElement method, TypeElement de * and no method can be called on such a receiver. Writing it is always a mistake, whatever the * enclosing class's constructors declare, and whether or not the method has a body. * + *

    The Iterator hierarchy is not checked: its bottom qualifier is its positive qualifier, and + * writing that on a receiver is legitimate. + * * @param method a method declaration */ private void checkReceiverNotBottom(MethodTree method) { + if (!atypeFactory.hasNegativeCapability()) { + // This is the Iterator hierarchy. + return; + } if (method.getReceiverParameter() == null) { // The receiver qualifier was defaulted, so the programmer did not write the bottom // qualifier on it. return; } AnnotatedDeclaredType receiverType = atypeFactory.getAnnotatedType(method).getReceiverType(); - if (receiverType == null) { + if (receiverType == null || !atypeFactory.isRelevant(receiverType.getUnderlyingType())) { return; } AnnotationMirror receiverAnno = receiverType.getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); - if (receiverAnno == null) { - return; - } - // Every modifiability hierarchy contains a top qualifier, a positive qualifier, and a - // polymorphic qualifier. Every hierarchy but the Iterator one also contains a negative and a - // bottom qualifier. There is no predicate for the bottom annotation, so it is recognized by - // eliminating all the others. (In the Iterator hierarchy, the positive qualifier is the - // bottom qualifier, and writing it on a receiver is legitimate.) - if (AnnotationUtils.areSameByName(receiverAnno, atypeFactory.topAnnotation()) - || AnnotationUtils.areSameByName(receiverAnno, atypeFactory.polyCapability()) - || AnnotationUtils.areSameByName(receiverAnno, positiveCapability()) - || isNegativeCapability(receiverAnno)) { - return; + AnnotationMirror bottom = + atypeFactory.getQualifierHierarchy().getBottomAnnotation(atypeFactory.topAnnotation()); + if (receiverAnno != null && AnnotationUtils.areSameByName(receiverAnno, bottom)) { + checker.reportError(method, "bottom.annotation.on.receiver"); } - checker.reportError(method, "bottom.annotation.on.receiver"); } /** @@ -427,8 +424,8 @@ static boolean implIsUOE(MethodTree method, Types types, Elements elements) { // java.lang.UnsupportedOperationException does. TypeMirror unsupportedOperationException = TypesUtils.typeFromClass(UnsupportedOperationException.class, types, elements); - return types.isSubtype( - types.erasure(TreeUtils.typeOf(exception)), types.erasure(unsupportedOperationException)); + return TypesUtils.isErasedSubtype( + TreeUtils.typeOf(exception), unsupportedOperationException, types); } /** From 936569b365bfc19e980e7ad3da31577d940cfd78 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 10:00:16 -0700 Subject: [PATCH 304/308] fix stale comment in IdentityHashMapModifiableTest --- .../tests/modifiability/IdentityHashMapModifiableTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/checker/tests/modifiability/IdentityHashMapModifiableTest.java b/checker/tests/modifiability/IdentityHashMapModifiableTest.java index 97dc24a9dcb4..56c2727ce206 100644 --- a/checker/tests/modifiability/IdentityHashMapModifiableTest.java +++ b/checker/tests/modifiability/IdentityHashMapModifiableTest.java @@ -14,8 +14,11 @@ void testIdentityHashMap() { boolean removed = identityMap.remove("key", "value2"); } - // TODO: The checker does not yet know that a map's keySet(), values(), and entrySet() views - // support removal but not addition. + void testViews(@Modifiable IdentityHashMap m) { + m.keySet().remove("k"); // OK + // :: error: [method.invocation] + m.keySet().add("k"); + } void testEntries() { @Modifiable IdentityHashMap identityMap = new IdentityHashMap<>(); From 24d1e8dbaf228fc2d5c3ac9ee67f8a44669d3e2b Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 12:22:27 -0700 Subject: [PATCH 305/308] change vivifyingLHMap to return a modifiable map --- .../org/checkerframework/afu/scenelib/el/AExpression.java | 5 ++++- .../org/checkerframework/afu/scenelib/el/ATypeElement.java | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java index 9aa52d6ba1f3..d3446ca52508 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java @@ -7,6 +7,7 @@ import java.util.SortedMap; import java.util.TreeMap; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; +import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.Shrinkable; /** @@ -45,7 +46,9 @@ public class AExpression extends AElement { /** * The method's annotated lambda expressions; map key is the offset of the invokedynamic bytecode */ - public final @Shrinkable VivifyingMap funs = + @SuppressWarnings("modifiability:assignment") // The anonymous subclass's constructor result + // is the top qualifier, so it does not propagate the backing map's @Modifiable type. + public final @Modifiable VivifyingMap funs = new VivifyingMap<>(new LinkedHashMap<>()) { @Override public AMethod createValueFor(RelativeLocation k) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java index c5dc82a3c549..bfdc8fac6bbe 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ATypeElement.java @@ -7,7 +7,6 @@ import org.checkerframework.afu.scenelib.Annotation; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * An {@link AElement} that represents a type might have annotations on inner types ("generic/array" @@ -24,7 +23,7 @@ public class ATypeElement extends AElement { /** The annotated inner types; map key is the inner type location. */ - public final @Shrinkable VivifyingMap, ATypeElement> innerTypes = + public final @Modifiable VivifyingMap, ATypeElement> innerTypes = newVivifyingLHMap_ATE(); /** From 577cf93b714eaf71596b5d9f5e5f838191cdc7a7 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 12:44:31 -0700 Subject: [PATCH 306/308] fix CI --- .../ModifiabilityBaseAnnotatedTypeFactory.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java index f4ea0dce40f9..52365ea37a44 100644 --- a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -282,8 +282,7 @@ public AnnotationMirror canonicalAnnotation( AnnotationMirror annotation, @Nullable TypeMirror tm) { if (expandsModifiabilityAliases()) { TypeMirror bound = tm == null ? null : TypesUtils.upperBound(tm); - String name = AnnotationUtils.annotationName(annotation); - switch (name) { + switch (AnnotationUtils.annotationName(annotation)) { case MODIFIABLE_NAME, UNMODIFIABLE_NAME -> { boolean weaken = bound != null @@ -295,7 +294,9 @@ public AnnotationMirror canonicalAnnotation( if (weaken) { return topAnnotation(); } - return name.equals(MODIFIABLE_NAME) ? positiveCapability() : negativeCapability(); + return areSameByClass(annotation, Modifiable.class) + ? positiveCapability() + : negativeCapability(); } case POLY_MODIFIABLE_NAME -> { return bound != null && lacksCapability(bound, this::polyLacksCapability) From b4d961a2219cc591c6a81fdf6e876d49b2b0af9a Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 12:56:08 -0700 Subject: [PATCH 307/308] fix annotation in wrapperMap.java --- .../afu/scenelib/util/coll/VivifyingMap.java | 1 - .../afu/scenelib/util/coll/WrapperMap.java | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index 4acdae8e2188..b5693237c744 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -60,7 +60,6 @@ public V getVivify(K k) { * *

    The receiver must be {@code @Shrinkable}, because pruning removes entries from the map. */ - @SuppressWarnings("modifiability:assignment") // entrySet() is not @IteratorPolyMod. public void prune(@Shrinkable VivifyingMap this) { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 387b4c8bbefa..ee0349ffba72 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -4,10 +4,13 @@ import java.util.Map; import java.util.Set; import org.checkerframework.checker.modifiability.qual.Growable; +import org.checkerframework.checker.modifiability.qual.IteratorPolyMod; import org.checkerframework.checker.modifiability.qual.Modifiable; import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.PolyShrinkable; import org.checkerframework.checker.modifiability.qual.Replaceable; import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; import org.checkerframework.dataflow.qual.SideEffectsOnly; /** @@ -47,9 +50,15 @@ public boolean containsValue(Object value) { return back.containsValue(value); } - @SuppressWarnings("keyfor") // use of delegate object + @SuppressWarnings({ + "keyfor", // use of delegate object + "modifiability:return" // see comment in body + }) @Override - public @PolyModifiable Set> entrySet(@PolyModifiable WrapperMap this) { + public @IteratorPolyMod @PolyShrinkable @Ungrowable Set> entrySet( + @PolyModifiable WrapperMap this) { + // `back` is @Modifiable, so each of its views is at least as capable as the polymorphic + // qualifier requires, whatever this WrapperMap's qualifier is. return back.entrySet(); } @@ -63,9 +72,15 @@ public boolean isEmpty() { return back.isEmpty(); } - @SuppressWarnings("keyfor") // use of delegate object + @SuppressWarnings({ + "keyfor", // use of delegate object + "modifiability:return" // see comment in body + }) @Override - public @PolyModifiable Set keySet(@PolyModifiable WrapperMap this) { + public @IteratorPolyMod @PolyShrinkable @Ungrowable Set keySet( + @PolyShrinkable WrapperMap this) { + // `back` is @Modifiable, so each of its views is at least as capable as the polymorphic + // qualifier requires, whatever this WrapperMap's qualifier is. return back.keySet(); } @@ -98,8 +113,12 @@ public int size() { return back.size(); } + @SuppressWarnings("modifiability:return") // see comment in body @Override - public @PolyModifiable Collection values(@PolyModifiable WrapperMap this) { + public @IteratorPolyMod @PolyShrinkable @Ungrowable Collection values( + @PolyShrinkable WrapperMap this) { + // `back` is @Modifiable, so each of its views is at least as capable as the polymorphic + // qualifier requires, whatever this WrapperMap's qualifier is. return back.values(); } From 6fe052ff74c07aa2bd41a181c2dc8e581216f301 Mon Sep 17 00:00:00 2001 From: YukiYang31 Date: Wed, 30 Sep 2026 20:51:20 -0700 Subject: [PATCH 308/308] make vivifyingMap modifiable --- .../checkerframework/afu/scenelib/el/AExpression.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java index d3446ca52508..f8bd5261e9d1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java @@ -8,7 +8,6 @@ import java.util.TreeMap; import org.checkerframework.afu.scenelib.util.coll.VivifyingMap; import org.checkerframework.checker.modifiability.qual.Modifiable; -import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * Manages all annotations within expressions, that is, annotations on typecasts, instanceofs, and @@ -16,31 +15,31 @@ */ public class AExpression extends AElement { /** The method's annotated typecasts; map key is the offset of the checkcast bytecode. */ - public final @Shrinkable VivifyingMap typecasts = + public final @Modifiable VivifyingMap typecasts = ATypeElement.newVivifyingLHMap_ATE(); /** * The method's annotated "instanceof" tests; map key is the offset of the instanceof bytecode. */ - public final @Shrinkable VivifyingMap instanceofs = + public final @Modifiable VivifyingMap instanceofs = ATypeElement.newVivifyingLHMap_ATE(); /** The method's annotated "new" invocations; map key is the offset of the new bytecode. */ - public final @Shrinkable VivifyingMap news = + public final @Modifiable VivifyingMap news = ATypeElement.newVivifyingLHMap_ATE(); /** * A method invocation's annotated type arguments; map key is the offset of the invokestatic * bytecode */ - public final @Shrinkable VivifyingMap calls = + public final @Modifiable VivifyingMap calls = ATypeElement.newVivifyingLHMap_ATE(); /** * A member reference's annotated type parameters; map key is the offset of the invokestatic * bytecode */ - public final @Shrinkable VivifyingMap refs = + public final @Modifiable VivifyingMap refs = ATypeElement.newVivifyingLHMap_ATE(); /**