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 6fb18ec984ca..066b57f5b0ac 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,6 +4,7 @@ import java.util.List; import java.util.Set; import org.checkerframework.afu.scenelib.type.Type; +import org.checkerframework.checker.modifiability.qual.Growable; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -15,7 +16,7 @@ public class ConstructorInsertion extends TypedInsertion { private @Nullable ReceiverInsertion receiverInsertion = null; /** The declaration annotations to write on the generated constructor. */ - private Set declarationInsertions = new LinkedHashSet<>(); + private @Growable Set declarationInsertions = new LinkedHashSet<>(); /** * Construct a ConstructorInsertion. @@ -28,7 +29,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 4079b228c7b2..65a6df972e58 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 8eee76b8a1e4..e0f102b79287 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.checkerframework.checker.nullness.qual.Nullable; import org.objectweb.asm.TypePath; import org.plumelib.util.StringsP; @@ -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 f07c71601248..a4e9f21c43ce 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,7 +29,7 @@ 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; /** * Creates a new {@code TypedInsertion} whose text is not on its own line. @@ -37,7 +38,8 @@ public abstract class TypedInsertion extends Insertion { * @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) { + public TypedInsertion( + Type type, Criteria criteria, @Growable List innerTypeInsertions) { this(type, criteria, false, innerTypeInsertions); } @@ -50,7 +52,7 @@ public TypedInsertion(Type type, Criteria criteria, List innerTypeIns * @param innerTypeInsertions the inner types to go on this insertion */ 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; @@ -83,7 +85,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 cc97bcaf83ab..6d09d99b30a3 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.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; import org.objectweb.asm.ClassReader; @@ -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 7c86433dc936..473a33e0bae4 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 @@ -14,6 +14,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; @@ -114,7 +116,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/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 e1c3917f6f3f..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 @@ -13,16 +13,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.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 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(); /** @@ -31,19 +35,22 @@ 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 @Shrinkable VivifyingMap methods = createMethodMap(); /** The class's annotated static initializer blocks; map key is the index of the block. */ - public final 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 VivifyingMap instanceInits = createInitBlockMap(); + public final @Growable @Shrinkable VivifyingMap instanceInits = + createInitBlockMap(); /** The class's annotated fields; map key is field name. */ - public final VivifyingMap fields = AField.newVivifyingLHMap_AF(); + public final @Growable @Shrinkable VivifyingMap fields = + AField.newVivifyingLHMap_AF(); /** The class's annotated field initializers; map key is field name. */ - public final VivifyingMap fieldInits = createFieldInitMap(); + public final @Growable VivifyingMap fieldInits = createFieldInitMap(); /** * The type element representing the class. Clients must call {@link #setTypeElement(TypeElement)} @@ -55,7 +62,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; @@ -63,15 +70,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<>(); @@ -244,7 +251,9 @@ public R accept(ElementVisitor v, T t) { * * @return a new vivifying map from method signature to {@link AMethod} */ - private static VivifyingMap createMethodMap() { + @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 public AMethod createValueFor(String k) { @@ -264,7 +273,9 @@ public boolean isEmptyValue(AMethod v) { * * @return a new vivifying map from initializer block index to {@link ABlock} */ - private static VivifyingMap createInitBlockMap() { + @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 public ABlock createValueFor(Integer k) { @@ -284,7 +295,9 @@ public boolean isEmptyValue(ABlock v) { * * @return a new vivifying map from field name to field initializer */ - private static VivifyingMap createFieldInitMap() { + @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 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 31ef4e0dce74..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 @@ -5,13 +5,16 @@ 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 = + @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 public ATypeElement createValueFor(ASTPath k) { @@ -28,7 +31,9 @@ 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 = + @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 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 0ec42c91c437..639a2a20f899 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 @@ -7,6 +7,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 @@ -22,7 +25,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? /** @@ -201,7 +204,9 @@ 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 */ - static VivifyingMap newVivifyingLHMap_AE() { + @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 public AElement createValueFor(K k) { @@ -245,7 +250,7 @@ public boolean isEmptyValue(AElement v) { // `@Nullable Object`, to `WrapperMap.get(Object)`, whose parameter is `@NonNull`. }) 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/AExpression.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AExpression.java index 8aff47b820ac..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 @@ -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; /** * Manages all annotations within expressions, that is, annotations on typecasts, instanceofs, and @@ -14,37 +15,39 @@ */ 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 @Modifiable 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 @Modifiable 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 @Modifiable 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 @Modifiable 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 @Modifiable VivifyingMap refs = ATypeElement.newVivifyingLHMap_ATE(); /** * The method's annotated lambda expressions; map key is the offset of the invokedynamic bytecode */ - public final 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/AField.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AField.java index 7ee0bd2f1bc1..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 @@ -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 { @@ -165,7 +166,9 @@ 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 */ - static VivifyingMap newVivifyingLHMap_AF() { + @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 public AField createValueFor(K 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/AScene.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/AScene.java index c343144d9b10..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 @@ -8,6 +8,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 @@ -45,7 +46,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 @@ -57,7 +59,9 @@ 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") // 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 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 5590e7164099..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 @@ -6,6 +6,7 @@ import java.util.Objects; 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" @@ -22,7 +23,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 @Modifiable VivifyingMap, ATypeElement> innerTypes = + newVivifyingLHMap_ATE(); /** * Construct a new ATypeElement from its description. @@ -118,7 +120,9 @@ 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 */ - static VivifyingMap newVivifyingLHMap_ATE() { + @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 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 82724eb47860..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 @@ -2,6 +2,8 @@ import java.util.Iterator; import java.util.Map; +import org.checkerframework.checker.modifiability.qual.PolyModifiable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; /** * A {@link VivifyingMap} is a map with two additional methods: @@ -25,7 +27,7 @@ public abstract class VivifyingMap extends WrapperMap { * * @param back the backing map */ - public VivifyingMap(Map back) { + public @PolyModifiable VivifyingMap(@PolyModifiable Map back) { super(back); } @@ -53,12 +55,16 @@ public V getVivify(K k) { */ protected abstract V createValueFor(K k); - /** Prunes this map by deleting entries with empty values. */ - public void prune() { + /** + * Prunes this map by deleting entries with empty values. + * + *

The receiver must be {@code @Shrinkable}, because pruning removes entries from the map. + */ + public void prune(@Shrinkable VivifyingMap this) { // 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 d7a1da9c4327..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 @@ -3,6 +3,14 @@ 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.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; /** @@ -15,20 +23,20 @@ */ 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. * * @param back the backing map */ - protected WrapperMap(Map back) { + protected WrapperMap(@Modifiable Map back) { this.back = back; } @Override @SideEffectsOnly("this") - public void clear() { + public void clear(@Shrinkable WrapperMap this) { back.clear(); } @@ -42,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 Set> entrySet() { + 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(); } @@ -58,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 Set keySet() { + 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(); } @@ -70,20 +90,21 @@ public Set keySet() { "nullness:return" // generics lower bound problem }) @SideEffectsOnly("this") - public V put(K key, V value) { + public V put(@Growable @Replaceable WrapperMap this, K key, V value) { return back.put(key, value); } @Override @SideEffectsOnly("this") - public void putAll(Map m) { + public void putAll( + @Growable @Replaceable WrapperMap this, Map m) { back.putAll(m); } @Override @SuppressWarnings("nullness:return") // generics lower bound problem @SideEffectsOnly("this") - public V remove(Object key) { + public V remove(@Shrinkable WrapperMap this, Object key) { return back.remove(key); } @@ -92,8 +113,12 @@ public int size() { return back.size(); } + @SuppressWarnings("modifiability:return") // see comment in body @Override - public Collection values() { + 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(); } diff --git a/build.gradle b/build.gradle index bddebacea5cc..d9c56f7b99df 100644 --- a/build.gradle +++ b/build.gradle @@ -990,6 +990,17 @@ subprojects { "org.checkerframework.checker.resourceleak.ResourceLeakChecker") rootProject.createCheckTypeTask(project.name, "Signature", "org.checkerframework.checker.signature.SignatureChecker") + 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 enforces nothing, but it does detect a + // crash of the checker. Remove `-Awarns` when the codebase issues no modifiability + // errors. + "-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. @@ -1206,7 +1217,7 @@ subprojects { tasks.register("typecheck-part1") { description = "Run the Checker Framework on itself (part 1)" group = "Verification" - dependsOn("checkCompilerMessages", "checkFormatter", "checkInterning", "checkOptional") + dependsOn("checkCompilerMessages", "checkFormatter", "checkInterning", "checkModifiability", "checkOptional") // Can't run "checkPurity" because it issues too many warnings. } tasks.register("typecheck-part2") { diff --git a/buildSrc/src/main/groovy/cf-spotless.gradle b/buildSrc/src/main/groovy/cf-spotless.gradle index 76900501e818..959f8cf7de10 100644 --- a/buildSrc/src/main/groovy/cf-spotless.gradle +++ b/buildSrc/src/main/groovy/cf-spotless.gradle @@ -92,6 +92,34 @@ spotless { googleJavaFormat(libs.google.java.format.get().version) } formatAnnotations() + .addTypeAnnotation("BottomGrowable") + .addTypeAnnotation("BottomReplaceable") + .addTypeAnnotation("BottomSeqGrowable") + .addTypeAnnotation("BottomShrinkable") + .addTypeAnnotation("Growable") + .addTypeAnnotation("IteratorPolyMod") + .addTypeAnnotation("MaybeGrowable") + .addTypeAnnotation("MaybeIteratorPolyMod") + .addTypeAnnotation("MaybeModifiable") + .addTypeAnnotation("MaybeReplaceable") + .addTypeAnnotation("MaybeSeqGrowable") + .addTypeAnnotation("MaybeShrinkable") + .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("Unmodifiable") + .addTypeAnnotation("UnmodifiableParam") + .addTypeAnnotation("Unreplaceable") + .addTypeAnnotation("Unshrinkable") } // Only define a groovyGradle task on the root project, for simplicity in setting the target pattern. 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/formatter/qual/ConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java index d6fb615cac07..3bf37fea0ab0 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; @@ -218,7 +219,7 @@ public static ConversionCategory fromConversionChar(char c) { * @param the type of array and set elements * @return a set containing the array's elements */ - 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 dc21f1a33b60..165eba213374 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; @@ -96,7 +97,7 @@ public static I18nConversionCategory stringToI18nConversionCategory(String strin * @param the type of array and set elements * @return a set containing the array's elements */ - 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/modifiability/qual/BottomGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrowable.java new file mode 100644 index 000000000000..74583859f068 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomGrowable.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 BottomGrowable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplaceable.java new file mode 100644 index 000000000000..13bcf09aa392 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomReplaceable.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 BottomReplaceable {} 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/BottomShrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrinkable.java new file mode 100644 index 000000000000..b27c14d30ad7 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/BottomShrinkable.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 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 new file mode 100644 index 000000000000..4ad15fd4f8be --- /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 + * 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(MaybeGrowable.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 new file mode 100644 index 000000000000..d770f7c97e98 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/IteratorPolyMod.java @@ -0,0 +1,34 @@ +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; + +/** + * If a collection's type is {@code @IteratorPolyMod}, then its {@code iterator()} and {@code + * 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 + * l.listIterator()} also has type {@code @Replaceable}. + * + *

If the collection itself is {@code @Unmodifiable}, then its iterator is {@code @Unshrinkable}. + * 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. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(MaybeIteratorPolyMod.class) +public @interface IteratorPolyMod {} 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 new file mode 100644 index 000000000000..c4914c496803 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeGrowable.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 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. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface MaybeGrowable {} 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 new file mode 100644 index 000000000000..b59bf80c2f7b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeIteratorPolyMod.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; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +/** + * The top qualifier in the Iterator hierarchy. The iterator of the annotated collection is not + * 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. + * + * @see IteratorPolyMod + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface MaybeIteratorPolyMod {} 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 new file mode 100644 index 000000000000..eafb5cce1344 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeModifiable.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; + +/** + * 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 @MaybeSeqGrowable @MaybeShrinkable @MaybeReplaceable} on each annotated + * type. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface MaybeModifiable {} 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 new file mode 100644 index 000000000000..1b1aa7978b95 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeReplaceable.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 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. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface MaybeReplaceable {} 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..27e7feb3dc8c --- /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 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. + * + * @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/MaybeShrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.java new file mode 100644 index 000000000000..2175b8951d5f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/MaybeShrinkable.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 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. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +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 new file mode 100644 index 000000000000..978239c8b045 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Modifiable.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 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 @MaybeSeqGrowable Iterator}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface Modifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrowable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrowable.java new file mode 100644 index 000000000000..fe6467671535 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyGrowable.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 @PolyGrowable}. + * + * @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(MaybeGrowable.class) +public @interface PolyGrowable {} 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 new file mode 100644 index 000000000000..f97cd75e264d --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyModifiable.java @@ -0,0 +1,38 @@ +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 @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()}. + * + *

For example: + * + *


+ * interface Example<E> {
+ *   @PolyModifiable List<E> subList(@PolyModifiable Example<E> this, int from, int to);
+ * }
+ * 
+ * + * 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 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface PolyModifiable {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplaceable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplaceable.java new file mode 100644 index 000000000000..b9f7b0fa5815 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyReplaceable.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 @PolyReplaceable}. + * + * @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(MaybeReplaceable.class) +public @interface PolyReplaceable {} 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/PolyShrinkable.java b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrinkable.java new file mode 100644 index 000000000000..9ed1fef83e6b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PolyShrinkable.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 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 + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(MaybeShrinkable.class) +public @interface PolyShrinkable {} 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..6bb2d47df6bd --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/PreservesModifiability.java @@ -0,0 +1,37 @@ +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 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. + * + *

It is an error to write this annotation on a method that does not have exactly one formal + * 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 + */ +@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-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..58a79dbd5b7a --- /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 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(MaybeReplaceable.class) +public @interface Replaceable {} 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..3714fe5909d9 --- /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}, {@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. + * + * @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..ff6c4008ed1f --- /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}, {@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. + * + * @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-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..39bb42266427 --- /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 removeAll}, {@code clear}, etc. on this + * collection will not 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(MaybeShrinkable.class) +public @interface Shrinkable {} 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-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..fba98d025e2f --- /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(MaybeGrowable.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 new file mode 100644 index 000000000000..9c4e5fb1dba6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/Unmodifiable.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; + +/** + * 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 @MaybeSeqGrowable @Unshrinkable @MaybeReplaceable Iterator}. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface Unmodifiable {} 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 new file mode 100644 index 000000000000..4c3390762cdc --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/modifiability/qual/UnmodifiableParam.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 that is syntactic sugar for {@code @}{@link MaybeModifiable}. It may only be + * written within a formal parameter 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 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface UnmodifiableParam {} 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..eec0205d2c50 --- /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(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 new file mode 100644 index 000000000000..eb32a3949516 --- /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 removeAll}, {@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(MaybeShrinkable.class) +public @interface Unshrinkable {} 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/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()) { diff --git a/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java new file mode 100644 index 000000000000..52365ea37a44 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseAnnotatedTypeFactory.java @@ -0,0 +1,516 @@ +package org.checkerframework.checker.modifiability; + +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; +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.checker.signature.qual.FullyQualifiedName; +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.AnnotationUtils; +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 { + + /** 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; + + /** 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; + + /** The erased {@code java.util.Iterator} type. */ + protected final TypeMirror iteratorErasure; + + /** The erased {@code java.util.ListIterator} type. */ + protected final TypeMirror listIteratorErasure; + + /** + * 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. + * + * @param checker the associated type-checker + */ + 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"); + 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)); + } + + /** + * 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( + @UnderInitialization(BaseAnnotatedTypeFactory.class) ModifiabilityBaseAnnotatedTypeFactory this, + @FullyQualifiedName 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 {@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; it is an upper bound, so it 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 type} structurally cannot support this checker's capability + */ + 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} + * 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; it is an upper bound, so it + * 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}, + * {@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. + * + *

{@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}. + * + *

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}. 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); + switch (AnnotationUtils.annotationName(annotation)) { + 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 areSameByClass(annotation, Modifiable.class) + ? positiveCapability() + : negativeCapability(); + } + case POLY_MODIFIABLE_NAME -> { + return bound != null && lacksCapability(bound, this::polyLacksCapability) + ? topAnnotation() + : polyCapability(); + } + case MAYBE_MODIFIABLE_NAME, UNMODIFIABLE_PARAM_NAME -> { + return topAnnotation(); + } + default -> { + // Not a whole-modifiability alias. + } + } + } + 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. + * + * @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); + } + + // 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 (inferTypeArgs && getDeclAnnotation(invokedMethod, PreservesModifiability.class) != null) { + refineReturnTypeForPreservesModifiability(tree, method); + } + + return mType; + } + + /** + * Refines the return type of a {@code @PreservesModifiability} method. + * + *

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 + * 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. + * + * @param tree an invocation of a {@code @PreservesModifiability} method + * @param methodType the annotated executable type of the invoked method + */ + 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. (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 = + 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)); + // `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()); + } + } + + /** + * 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. + * + *

A declared negative result keeps its declared qualifier, since such an iterator never has + * 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. + * + *

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}. + * + * @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. + return; + } + AnnotatedTypeMirror returnType = methodType.getReturnType(); + // 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; + } + // 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; + } + + // `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; + } + + // 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.hasAnnotation(negativeCapability())) { + returnType.replaceAnnotation(negativeCapability()); + return; + } + + // The receiver has the capability; its iterator does too if the receiver is @IteratorPolyMod. + if (receiverType.hasAnnotation(positiveCapability())) { + AnnotatedTypeMirror iteratorHierarchyType = + getTypeFactoryOfSubchecker(IteratorChecker.class).getReceiverType(tree); + if (iteratorHierarchyType != null && iteratorHierarchyType.hasAnnotation(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()); + } + + /** + * 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 an iterator method + */ + private boolean isIteratorMethodInvocation(MethodInvocationTree tree) { + return TreeUtils.isMethodInvocation(tree, iteratorMethods, processingEnv); + } +} 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..6b3fe06e12a5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseChecker.java @@ -0,0 +1,81 @@ +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; +import org.checkerframework.framework.source.SourceChecker; + +/** + * Base class for the Modifiability sub-checkers. + * + *

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}, + * 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}. + *
  • The Iterator Checker as a subchecker; see {@link #usesIteratorChecker}. + *
+ */ +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", + "/org/checkerframework/checker/modifiability/jdk.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 + * 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 (usesIteratorChecker()) { + 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 new file mode 100644 index 000000000000..f4274a460062 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityBaseVisitor.java @@ -0,0 +1,520 @@ +package org.checkerframework.checker.modifiability; + +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +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 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.element.Modifier; +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; +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.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +/** + * Base visitor for the modifiability sub-checkers (Grow, SeqGrow, Shrink, Replace, and Iterator). + * + *

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}. + *
  • 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 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 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. + *
+ */ +public class ModifiabilityBaseVisitor + extends BaseTypeVisitor { + + /** + * Create a ModifiabilityBaseVisitor. + * + * @param checker the checker that uses this visitor + */ + 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 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: it does nothing unless some + * constructor of the class declares a qualifier in this hierarchy. + */ + @Override + protected void checkThisOrSuperConstructorCall( + MethodInvocationTree call, @CompilerMessageKey String errorKey) { + // Do nothing. + } + + @Override + public void processClassTree(ClassTree tree) { + super.processClassTree(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 the constructors against one another and against the + * method bodies. + * + * @param tree a class + */ + private void processClassMembers(ClassTree tree) { + 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; + } + + 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); + } + } + } + + 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 (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)) { + checker.reportError( + constructor, "inconsistent.constructor.result.type", thisResultAnno, constructorAnno); + 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 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; + } + AnnotationMirror receiverAnno = receiverCapabilityRequirement(method); + if (receiverAnno != null) { + checkImplOK(method, receiverAnno, constructorAnno); + } + } + + checkInheritedImplementations(tree, classElement, constructorAnno); + } + + /** + * 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 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 + */ + private void checkInheritedImplementations( + ClassTree tree, TypeElement classElement, AnnotationMirror constructorAnno) { + if (!atypeFactory.hasNegativeCapability() + || !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 + // 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)) { + // 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(atypeFactory.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 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 = + atypeFactory + .getAnnotatedType(constructor) + .getReturnType() + .getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); + if (resultAnno == null) { + continue; + } + if (!isNegativeCapability(resultAnno)) { + return false; + } + sawNegative = true; + } + return sawNegative; + } + + /** + * 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. + * + *

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 || !atypeFactory.isRelevant(receiverType.getUnderlyingType())) { + return; + } + AnnotationMirror receiverAnno = + receiverType.getPrimaryAnnotationInHierarchy(atypeFactory.topAnnotation()); + AnnotationMirror bottom = + atypeFactory.getQualifierHierarchy().getBottomAnnotation(atypeFactory.topAnnotation()); + if (receiverAnno != null && AnnotationUtils.areSameByName(receiverAnno, bottom)) { + 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. + * + *

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(); + 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(atypeFactory.positiveCapability())) { + return atypeFactory.positiveCapability(); + } + } + return null; + } + + /** + * 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 + */ + private void checkImplOK( + MethodTree method, AnnotationMirror receiverAnno, AnnotationMirror constructorAnno) { + 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.) + return; + } + + // `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()) { + return; + } + String constructorAnnoName = + constructorAnno.getAnnotationType().asElement().getSimpleName().toString(); + if (isNegativeCapability(constructorAnno)) { + if (!implIsUOE(method, types, elements)) { + checker.reportError(method, "method.implementation.not.uoe", constructorAnnoName); + } + } else if (AnnotationUtils.areSameByName(constructorAnno, atypeFactory.positiveCapability())) { + if (implIsUOE(method, types, elements)) { + 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. + } + + /** + * 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 throw new + * 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(...)} + */ + 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. + return false; + } + 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)) { + return false; + } + // 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 TypesUtils.isErasedSubtype( + TreeUtils.typeOf(exception), unsupportedOperationException, types); + } + + /** + * 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. + * + *

For example: + * + *

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

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( + MethodTree overriderTree, + AnnotatedExecutableType overriderMethodType, + AnnotatedDeclaredType overriderType, + AnnotatedExecutableType overriddenMethodType, + AnnotatedDeclaredType overriddenType) { + if (!super.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(); + if (overriderReceiver == null || overriddenReceiver == null) { + return true; + } + + 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); + checker.reportError( + overriderTree, + "override.receiver", + pair.found, + pair.required, + overriderType, + overriderMethodType, + overriddenType, + overriddenMethodType); + return false; + } + 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; + } + + // 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 + // 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/ModifiabilityChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java new file mode 100644 index 000000000000..d5862182940c --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityChecker.java @@ -0,0 +1,51 @@ +package org.checkerframework.checker.modifiability; + +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; +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 + * UnsupportedOperationException} at run time due to calling a mutating method on a collection. + * + *

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}) + *
  • {@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}) + *
  • {@link IteratorChecker} checks whether iterators preserve receiver modifiability + *
+ * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +public class ModifiabilityChecker extends AggregateChecker { + + /** Creates a ModifiabilityChecker. */ + public ModifiabilityChecker() {} + + @Override + protected Collection> getSupportedCheckers() { + return List.of( + GrowChecker.class, + SeqGrowChecker.class, + ShrinkChecker.class, + ReplaceChecker.class, + IteratorChecker.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..ec103e423b58 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/ModifiabilityVisitor.java @@ -0,0 +1,163 @@ +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; +import org.checkerframework.javacutil.TreeUtils; + +/** + * 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}. */ + 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; + + /** 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. + * + * @param checker the modifiability checker to invoke on the input source tree + */ + protected ModifiabilityVisitor(ModifiabilityChecker checker) { + super(checker); + this.checker = checker; + } + + /** + * 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 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); + inParameterType = true; + try { + scan(tree.getParameters(), p); + scan(tree.getReceiverParameter(), p); + } finally { + inParameterType = false; + } + scan(tree.getThrows(), p); + scan(tree.getBody(), p); + scan(tree.getDefaultValue(), 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. + * + * @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 + * result. + * + *

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 + */ + 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.isVarArgs() + && 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 + // 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/grow/GrowAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java new file mode 100644 index 000000000000..6334aac42510 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowAnnotatedTypeFactory.java @@ -0,0 +1,119 @@ +package org.checkerframework.checker.modifiability.grow; + +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; +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.PolyGrowable; +import org.checkerframework.checker.modifiability.qual.Ungrowable; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TypesUtils; + +/** The annotated type factory for the {@link GrowChecker}. */ +public class GrowAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { + + /** The {@code @}{@link MaybeGrowable} qualifier. */ + private final AnnotationMirror MAYBE_GROWABLE; + + /** The {@code @}{@link Growable} qualifier. */ + private final AnnotationMirror GROWABLE; + + /** The {@code @}{@link Ungrowable} qualifier. */ + private final AnnotationMirror UNGROWABLE; + + /** The {@code @}{@link PolyGrowable} qualifier. */ + private final AnnotationMirror POLY_GROWABLE; + + /** + * Creates a GrowAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + public GrowAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + 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(); + } + + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_GROWABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return GROWABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return UNGROWABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_GROWABLE; + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + MaybeGrowable.class, + Growable.class, + Ungrowable.class, + BottomGrowable.class, + PolyGrowable.class)); + } + + /** + * {@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 + protected boolean typeLacksCapability(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)); + } + + @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}. + */ + @Override + protected boolean polyLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); + } + + @Override + 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/GrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java new file mode 100644 index 000000000000..9eb84bdbd9a3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/grow/GrowChecker.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.grow; + +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 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 @MaybeGrowable} collections + * have unknown grow behavior. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) +@SuppressWarningsPrefix({"growable", "modifiability"}) +public class GrowChecker extends ModifiabilityBaseChecker { + /** Creates a GrowChecker. */ + public GrowChecker() {} +} 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..0791ab0f91d2 --- /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 {} 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..30d8e05028b7 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorAnnotatedTypeFactory.java @@ -0,0 +1,82 @@ +package org.checkerframework.checker.modifiability.iterator; + +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; +import org.checkerframework.checker.modifiability.qual.PolyIteratorPolyMod; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationBuilder; + +/** 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 PolyIteratorPolyMod} qualifier. */ + private final AnnotationMirror POLY_ITERATOR_POLY_MOD; + + /** + * Creates an IteratorAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + public IteratorAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.MAYBE_ITERATOR_POLY_MOD = + AnnotationBuilder.fromClass(elements, MaybeIteratorPolyMod.class); + this.POLY_ITERATOR_POLY_MOD = AnnotationBuilder.fromClass(elements, PolyIteratorPolyMod.class); + postInit(); + } + + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_ITERATOR_POLY_MOD; + } + + @Override + protected AnnotationMirror positiveCapability() { + return ITERATOR_POLY_MOD; + } + + @Override + 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; + } + + @Override + 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<>( + Arrays.asList( + MaybeIteratorPolyMod.class, IteratorPolyMod.class, PolyIteratorPolyMod.class)); + } +} 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..313d5f561fb8 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorChecker.java @@ -0,0 +1,31 @@ +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() {} + + @Override + protected boolean usesIteratorChecker() { + return false; + } +} 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..f68a6141afbc --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/iterator/IteratorVisitor.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.iterator; + +import org.checkerframework.checker.modifiability.ModifiabilityBaseVisitor; +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** Visitor for the {@link IteratorChecker}. */ +public class IteratorVisitor extends ModifiabilityBaseVisitor { + + /** + * 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; + } +} 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/src/main/java/org/checkerframework/checker/modifiability/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/modifiability/javaparser.astub new file mode 100644 index 000000000000..cc63c87a8309 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/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/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 new file mode 100644 index 000000000000..bcf7df473b8d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/messages.properties @@ -0,0 +1,8 @@ +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/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..602d0cef1ee1 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceAnnotatedTypeFactory.java @@ -0,0 +1,126 @@ +package org.checkerframework.checker.modifiability.replace; + +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; +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.MaybeReplaceable; +import org.checkerframework.checker.modifiability.qual.PolyReplaceable; +import org.checkerframework.checker.modifiability.qual.Replaceable; +import org.checkerframework.checker.modifiability.qual.Unreplaceable; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TypesUtils; + +/** The annotated type factory for the {@link ReplaceChecker}. */ +public class ReplaceAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { + + /** 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.List} type. */ + private final TypeMirror listErasure; + + /** The {@code @}{@link MaybeReplaceable} qualifier. */ + private final AnnotationMirror MAYBE_REPLACEABLE; + + /** The {@code @}{@link Replaceable} qualifier. */ + private final AnnotationMirror REPLACEABLE; + + /** The {@code @}{@link Unreplaceable} qualifier. */ + private final AnnotationMirror UNREPLACEABLE; + + /** The {@code @}{@link PolyReplaceable} qualifier. */ + private final AnnotationMirror POLY_REPLACEABLE; + + /** + * Creates a ReplaceAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + public ReplaceAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.setErasure = erasureOf("java.util.Set"); + this.queueErasure = erasureOf("java.util.Queue"); + 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); + this.POLY_REPLACEABLE = AnnotationBuilder.fromClass(elements, PolyReplaceable.class); + postInit(); + } + + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_REPLACEABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return REPLACEABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return UNREPLACEABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_REPLACEABLE; + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + MaybeReplaceable.class, + Replaceable.class, + Unreplaceable.class, + BottomReplaceable.class, + PolyReplaceable.class)); + } + + /** + * 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. + */ + @Override + protected boolean typeLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return types.isSameType(types.erasure(type), collectionErasure) + || TypesUtils.isErasedSubtype(type, setErasure, types) + || (TypesUtils.isErasedSubtype(type, queueErasure, 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. + + @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/ReplaceChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java new file mode 100644 index 000000000000..f91c908d26ec --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/replace/ReplaceChecker.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.replace; + +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 replace method on a collection. + * + *

The checker enforces the Modifiability type system, where {@code @Replaceable} collections can + * be safely mutated, {@code @Unreplaceable} collections cannot be replaced, and + * {@code @MaybeReplaceable} collections have unknown replace behavior. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) +@SuppressWarningsPrefix({"replaceable", "modifiability"}) +public class ReplaceChecker extends ModifiabilityBaseChecker { + /** Creates a Replace checker. */ + public ReplaceChecker() {} +} 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..bde38f661de4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowAnnotatedTypeFactory.java @@ -0,0 +1,158 @@ +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; +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; +import org.checkerframework.checker.modifiability.qual.PolySeqGrowable; +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.FullyQualifiedName; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationBuilder; +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, or null on a JDK before Java 21. */ + private final @Nullable TypeMirror sequencedCollectionErasure; + + /** 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. */ + private final TypeMirror dequeErasure; + + /** 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 = 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); + 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( + @UnderInitialization(ModifiabilityBaseAnnotatedTypeFactory.class) SeqGrowAnnotatedTypeFactory this, + @FullyQualifiedName 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; + } + + @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)); + } + + /** + * 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. + * + *

On JDKs before Java 21, {@code SequencedCollection} and {@code SequencedMap} are not + * present, but {@code Deque} still has first/last insertion methods. + */ + @Override + protected boolean typeLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return true; + } + boolean canSeqGrow = + (sequencedCollectionErasure != null + && TypesUtils.isErasedSubtype(type, sequencedCollectionErasure, types)) + || (sequencedMapErasure != null + && TypesUtils.isErasedSubtype(type, sequencedMapErasure, types)) + || TypesUtils.isErasedSubtype(type, dequeErasure, types); + 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}. + */ + @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/seqgrow/SeqGrowChecker.java b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java new file mode 100644 index 000000000000..5d64ce4ea6a3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/seqgrow/SeqGrowChecker.java @@ -0,0 +1,32 @@ +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() {} + + @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/shrink/ShrinkAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java new file mode 100644 index 000000000000..780383b63072 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkAnnotatedTypeFactory.java @@ -0,0 +1,117 @@ +package org.checkerframework.checker.modifiability.shrink; + +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; +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.MaybeShrinkable; +import org.checkerframework.checker.modifiability.qual.PolyShrinkable; +import org.checkerframework.checker.modifiability.qual.Shrinkable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TypesUtils; + +/** The annotated type factory for the {@link ShrinkChecker}. */ +public class ShrinkAnnotatedTypeFactory extends ModifiabilityBaseAnnotatedTypeFactory { + + /** The {@code @}{@link MaybeShrinkable} qualifier. */ + private final AnnotationMirror MAYBE_SHRINKABLE; + + /** The {@code @}{@link Shrinkable} qualifier. */ + private final AnnotationMirror SHRINKABLE; + + /** The {@code @}{@link Unshrinkable} qualifier. */ + private final AnnotationMirror UNSHRINKABLE; + + /** The {@code @}{@link PolyShrinkable} qualifier. */ + private final AnnotationMirror POLY_SHRINKABLE; + + /** + * Creates a ShrinkAnnotatedTypeFactory. + * + * @param checker the associated type-checker + */ + @SuppressWarnings("this-escape") + public ShrinkAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + 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(); + } + + @Override + protected AnnotationMirror topAnnotation() { + return MAYBE_SHRINKABLE; + } + + @Override + protected AnnotationMirror positiveCapability() { + return SHRINKABLE; + } + + @Override + protected AnnotationMirror negativeCapability() { + return UNSHRINKABLE; + } + + @Override + protected AnnotationMirror polyCapability() { + return POLY_SHRINKABLE; + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + MaybeShrinkable.class, + Shrinkable.class, + Unshrinkable.class, + BottomShrinkable.class, + PolyShrinkable.class)); + } + + /** + * {@code Map.Entry} has no shrink methods, so {@code @Modifiable} and {@code @Unmodifiable} make + * no claim about it. + */ + @Override + protected boolean typeLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + 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}. + */ + @Override + protected boolean polyLacksCapability(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + return TypesUtils.isErasedSubtype(type, mapEntryErasure, types); + } + + @Override + protected @Nullable TypeMirror refinedIteratorResultBound() { + // Every Iterator can shrink, via remove(). + return iteratorErasure; + } +} 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..46b799dce40b --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/modifiability/shrink/ShrinkChecker.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.modifiability.shrink; + +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 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 @MaybeShrinkable} collections have unknown shrink behavior. + * + * @checker_framework.manual #modifiability-checker Modifiability Checker + */ +@RelevantJavaTypes({Collection.class, Iterator.class, Map.class, Map.Entry.class}) +@SuppressWarningsPrefix({"shrinkable", "modifiability"}) +public class ShrinkChecker extends ModifiabilityBaseChecker { + /** Creates a Shrink checker. */ + public ShrinkChecker() {} +} 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"}; + } +} diff --git a/checker/tests/modifiability/AliasCanonicalizationTest.java b/checker/tests/modifiability/AliasCanonicalizationTest.java new file mode 100644 index 000000000000..30b3420c7e89 --- /dev/null +++ b/checker/tests/modifiability/AliasCanonicalizationTest.java @@ -0,0 +1,76 @@ +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.IteratorPolyMod; +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.Unshrinkable; + +class AliasCanonicalizationTest { + + void modifiableIterator(@Modifiable Iterator iterator) { + @MaybeGrowable @Shrinkable @MaybeReplaceable Iterator canonical = iterator; + @Shrinkable Iterator shrinkable = iterator; + iterator.remove(); + } + + void unmodifiableIterator(@Unmodifiable Iterator iterator) { + @MaybeGrowable @Unshrinkable @MaybeReplaceable Iterator canonical = iterator; + // :: error: [method.invocation] + iterator.remove(); + } + + void modifiableSet(@Modifiable Set set) { + @Growable @Shrinkable @MaybeReplaceable Set canonical = set; + set.add("a"); + set.remove("a"); + } + + void modifiableQueue(@Modifiable Queue queue) { + @Growable @Shrinkable @MaybeReplaceable Queue canonical = queue; + queue.add("a"); + queue.remove(); + } + + void modifiableCollection(@Modifiable @IteratorPolyMod Collection collection) { + @Growable @Shrinkable @IteratorPolyMod @MaybeReplaceable Collection canonical = collection; + collection.add("a"); + collection.remove("a"); + } + + void modifiableEntry(Map.@Modifiable Entry entry) { + Map.@MaybeGrowable @MaybeShrinkable @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/BasicModifiabilityTest.java b/checker/tests/modifiability/BasicModifiabilityTest.java new file mode 100644 index 000000000000..c26ef4ddbb4e --- /dev/null +++ b/checker/tests/modifiability/BasicModifiabilityTest.java @@ -0,0 +1,130 @@ +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.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +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 { + + 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, + @MaybeModifiable List anyList) { + + modList.toArray(); + unmodList.toArray(); + anyList.toArray(); + + String[] fromMod = modList.toArray(new String[0]); + String[] fromUnmod = unmodList.toArray(new String[0]); + String[] fromAny = anyList.toArray(new String[0]); + } + + void testIteratorPolymorphic( + @Modifiable List modList, @Unmodifiable List unmodList) { + + @MaybeModifiable 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 @IteratorPolyMod List mod, @Unmodifiable List unmod) { + List other = new ArrayList<>(); + other.add("x"); + + 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); + } + + void testUnmodifiableParamMutation(@UnmodifiableParam 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 + "!"); + + // :: error: [method.invocation] + unmod.replaceAll(s -> s + "!"); + } + + void testSubList(@Modifiable List mod, @Unmodifiable List unmod) { + List sub = mod.subList(0, mod.size()); + 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/BasicModifiableTest3.java b/checker/tests/modifiability/BasicModifiableTest3.java new file mode 100644 index 000000000000..1fd018b252fa --- /dev/null +++ b/checker/tests/modifiability/BasicModifiableTest3.java @@ -0,0 +1,23 @@ +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 { + + /* + @ReleasesNoLocks + // @SideEffectsOnly("this") + @EnsuresNonEmpty("this") + */ + 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: [argument] + my_add(unmodifiableList, "test3"); + } +} diff --git a/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java new file mode 100644 index 000000000000..e8f4dfb85e0a --- /dev/null +++ b/checker/tests/modifiability/ClassAnnosVSDefaultQualifierForUse.java @@ -0,0 +1,33 @@ +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. + 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) {} +} diff --git a/checker/tests/modifiability/CollectionModifiableTest.java b/checker/tests/modifiability/CollectionModifiableTest.java new file mode 100644 index 000000000000..0fe19540dcf4 --- /dev/null +++ b/checker/tests/modifiability/CollectionModifiableTest.java @@ -0,0 +1,84 @@ +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.IteratorPolyMod; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +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 @IteratorPolyMod Collection mod, + @Unmodifiable Collection unmod, + @MaybeModifiable 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"); + } +} diff --git a/checker/tests/modifiability/CollectionsModifiableTest.java b/checker/tests/modifiability/CollectionsModifiableTest.java new file mode 100644 index 000000000000..921122f50b59 --- /dev/null +++ b/checker/tests/modifiability/CollectionsModifiableTest.java @@ -0,0 +1,306 @@ +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.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 { + + 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 itor = Collections.emptyIterator(); + // :: error: [method.invocation] + itor.remove(); + // :: error: [assignment] + @Modifiable Iterator modItor = Collections.emptyIterator(); + } + + 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); + + @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<>(); + @Unmodifiable SortedMap unmodSM = Collections.unmodifiableSortedMap(sm); + // :: error: [method.invocation] + unmodSM.put("a", "b"); + } +} diff --git a/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java b/checker/tests/modifiability/ConcurrentHashMapModifiableTest.java new file mode 100644 index 000000000000..d0021d020aeb --- /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/ConstructorResultTypeTest.java b/checker/tests/modifiability/ConstructorResultTypeTest.java new file mode 100644 index 000000000000..34f749eaacbb --- /dev/null +++ b/checker/tests/modifiability/ConstructorResultTypeTest.java @@ -0,0 +1,108 @@ +// Tests the `inconsistent.constructor.result.type` and `bottom.annotation.on.receiver` checks, +// which are performed by ModifiabilityBaseVisitor.processClassTree. +// +// 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. + +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; + +public class ConstructorResultTypeTest { + + // 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() {} + + @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. + // :: error: [inherited.implementation.uoe] + 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) {} + } + + // 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); + } +} diff --git a/checker/tests/modifiability/CustomCollectionAnnotations.java b/checker/tests/modifiability/CustomCollectionAnnotations.java new file mode 100644 index 000000000000..d437be5adb58 --- /dev/null +++ b/checker/tests/modifiability/CustomCollectionAnnotations.java @@ -0,0 +1,179 @@ +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; +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.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 CustomCollectionAnnotations { + + static class ExtendsAbstractList extends AbstractList { + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + abstract static class DirectCollection implements Collection {} + + abstract static class DirectMap implements Map {} + + abstract static class DirectIterator implements Iterator {} + + 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) { + return "value"; + } + + @Override + public int size() { + return 1; + } + } + + // The implicit super() call is fine because ArrayList's constructor already + // returns @Modifiable, but the subclass annotation is still an unverified modifiability claim. + static @Modifiable class ModifiableSubclassWithoutSuperError extends ArrayList {} + + abstract static @Growable class ClassLevelGrowableCollection implements Collection {} + + 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(); + } + + // :: error: [inherited.implementation.uoe] + class KeySet extends AbstractSet { + + public @Modifiable KeySet() {} + + @Override + public Iterator iterator() { + return java.util.Collections.emptyIterator(); + } + + @Override + public int size() { + return 0; + } + } + } + + abstract static @IteratorPolyMod class ClassLevelIteratorPolyModIterator + implements Iterator {} + + // :: error: [inherited.implementation.uoe] + static class ConstructorLevelShrinkableList extends AbstractList { + @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; + } + } + + // :: error: [inherited.implementation.uoe] :: error: [inherited.implementation.uoe] :: error: + // [inherited.implementation.uoe] + static @Modifiable class SuppressedList extends AbstractList { + @Override + public String get(int index) { + return "value"; + } + + @Override + public int size() { + return 1; + } + + @Override + // no [override.receiver] error here because @Modifiable is an upper bound. + public boolean add(@IteratorPolyMod SuppressedList this, String e) { + return super.add(e); + } + } + + abstract static @Unmodifiable class SuppressedMap implements Map {} +} diff --git a/checker/tests/modifiability/ExactReceiverOverrideTest.java b/checker/tests/modifiability/ExactReceiverOverrideTest.java new file mode 100644 index 000000000000..6c19557a81a9 --- /dev/null +++ b/checker/tests/modifiability/ExactReceiverOverrideTest.java @@ -0,0 +1,172 @@ +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; + +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 { + + @Unmodifiable UnmodifiableArrayList() {} + + @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/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/GetOrDefaultTest.java b/checker/tests/modifiability/GetOrDefaultTest.java new file mode 100644 index 000000000000..58fa7d848d3c --- /dev/null +++ b/checker/tests/modifiability/GetOrDefaultTest.java @@ -0,0 +1,253 @@ +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; + +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); + + // 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); +} + +public class GetOrDefaultTest { + + List d; + @Modifiable List dm; + @Unmodifiable List 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) { + // :: 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) { + // :: 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) { + 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) { + 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) { + 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) { + 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) { + // :: 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) { + // :: 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); + } + + // 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); + } +} diff --git a/checker/tests/modifiability/HierarchyTest.java b/checker/tests/modifiability/HierarchyTest.java new file mode 100644 index 000000000000..042940913c44 --- /dev/null +++ b/checker/tests/modifiability/HierarchyTest.java @@ -0,0 +1,288 @@ +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; +import org.checkerframework.checker.modifiability.qual.Unreplaceable; +import org.checkerframework.checker.modifiability.qual.Unshrinkable; + +/** + * 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). + * + *

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 testGrow( + @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; + + // :: 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] + @BottomGrowable Object b1 = g; + // :: error: [assignment] + @BottomGrowable Object b2 = ug; + // :: error: [assignment] + @BottomGrowable Object b3 = u; + } + + void testShrink( + @Shrinkable Object s, + @Unshrinkable Object us, + @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; + + // :: error: [assignment] + @Shrinkable Object s2 = us; + // :: error: [assignment] + @Unshrinkable Object us2 = s; + // :: error: [assignment] + @Shrinkable Object s3 = u; + // :: error: [assignment] + @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, + @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; + + // :: error: [assignment] + @Replaceable Object r2 = ur; + // :: error: [assignment] + @Unreplaceable Object ur2 = r; + // :: error: [assignment] + @Replaceable Object r3 = u; + // :: error: [assignment] + @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, + @Growable @Replaceable Object gr, + @Shrinkable @Replaceable Object sr, + @Growable Object g, + @Shrinkable Object s, + @Replaceable Object r, + @Ungrowable @Unshrinkable @Unreplaceable Object none, + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Object unknown) { + + @Growable Object gv1 = gsr; + @Growable Object gv2 = gs; + @Growable Object gv3 = gr; + // :: error: [assignment] + @Growable Object gv4 = sr; + // :: error: [assignment] + @Growable Object gv5 = s; + // :: error: [assignment] + @Growable Object gv6 = r; + // :: error: [assignment] + @Growable Object gv7 = none; + // :: error: [assignment] + @Growable Object gv8 = unknown; + + @Shrinkable Object sv1 = gsr; + @Shrinkable Object sv2 = gs; + // :: error: [assignment] + @Shrinkable Object sv3 = gr; + @Shrinkable Object sv4 = sr; + // :: error: [assignment] + @Shrinkable Object sv5 = g; + // :: error: [assignment] + @Shrinkable Object sv6 = r; + // :: error: [assignment] + @Shrinkable Object sv7 = none; + // :: error: [assignment] + @Shrinkable Object sv8 = unknown; + + @Replaceable Object rv1 = gsr; + // :: error: [assignment] + @Replaceable Object rv2 = gs; + @Replaceable Object rv3 = gr; + @Replaceable Object rv4 = sr; + // :: error: [assignment] + @Replaceable Object rv5 = g; + // :: error: [assignment] + @Replaceable Object rv6 = s; + // :: error: [assignment] + @Replaceable Object rv7 = none; + // :: error: [assignment] + @Replaceable Object rv8 = 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( + @Modifiable Object mod, @Unmodifiable Object unmod, @MaybeModifiable Object unknown) { + + @Growable Object g1 = mod; + @Shrinkable Object s1 = mod; + @Replaceable Object r1 = mod; + + @Ungrowable Object ug1 = unmod; + @Unshrinkable Object us1 = unmod; + @Unreplaceable Object ur1 = unmod; + + @MaybeGrowable Object ug2 = unknown; + @MaybeSeqGrowable Object usg2 = unknown; + @MaybeShrinkable Object us2 = unknown; + @MaybeReplaceable Object ur2 = unknown; + + @MaybeModifiable Object unknown1 = mod; + @MaybeModifiable Object unknown2 = unmod; + + // :: error: [assignment] + @Unmodifiable Object unmod1 = mod; + // :: error: [assignment] + @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/IdentityHashMapModifiableTest.java b/checker/tests/modifiability/IdentityHashMapModifiableTest.java new file mode 100644 index 000000000000..56c2727ce206 --- /dev/null +++ b/checker/tests/modifiability/IdentityHashMapModifiableTest.java @@ -0,0 +1,44 @@ +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"); + } + + void testViews(@Modifiable IdentityHashMap m) { + m.keySet().remove("k"); // OK + // :: error: [method.invocation] + m.keySet().add("k"); + } + + 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 + entry.setValue("modified"); // OK + } + + // Stream/Spliterator returns immutable entries + if (!identityMap.isEmpty()) { + Map.Entry entry = identityMap.entrySet().stream().findFirst().get(); + // 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/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(); + } +} diff --git a/checker/tests/modifiability/InheritedImplementationTest.java b/checker/tests/modifiability/InheritedImplementationTest.java new file mode 100644 index 000000000000..762dfd6ce879 --- /dev/null +++ b/checker/tests/modifiability/InheritedImplementationTest.java @@ -0,0 +1,181 @@ +// 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) {} + } + + // 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 {} + + // 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/IntersectionBoundTest.java b/checker/tests/modifiability/IntersectionBoundTest.java new file mode 100644 index 000000000000..0cb8647b75ae --- /dev/null +++ b/checker/tests/modifiability/IntersectionBoundTest.java @@ -0,0 +1,40 @@ +// 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.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 IntersectionBoundTest { + + // `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; + } + + // `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/IteratorCheckerTest.java b/checker/tests/modifiability/IteratorCheckerTest.java new file mode 100644 index 000000000000..06b5fc3f1259 --- /dev/null +++ b/checker/tests/modifiability/IteratorCheckerTest.java @@ -0,0 +1,55 @@ +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.PolyIteratorPolyMod; +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); + } + + static @PolyIteratorPolyMod List identityIteratorPoly( + @PolyIteratorPolyMod List values) { + return values; + } + + void preservesIteratorPoly( + @IteratorPolyMod List iteratorPoly, @MaybeIteratorPolyMod List maybe) { + @IteratorPolyMod List preserved = annotated(iteratorPoly); + + // :: 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/IteratorConflictingAnnosTypeTuple.java b/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java new file mode 100644 index 000000000000..bba905fa38c7 --- /dev/null +++ b/checker/tests/modifiability/IteratorConflictingAnnosTypeTuple.java @@ -0,0 +1,18 @@ +// 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; +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..56775b7ce8e9 --- /dev/null +++ b/checker/tests/modifiability/IteratorConflictingAnnosUse.java @@ -0,0 +1,14 @@ +// 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; + +public class IteratorConflictingAnnosUse { + private final IteratorConflictingAnnosTypeTuple inputTypes = + new IteratorConflictingAnnosTypeTuple(); + + @Unmodifiable Iterator reproduce() { + return inputTypes.iterator(); + } +} diff --git a/checker/tests/modifiability/IteratorPrecisionTest.java b/checker/tests/modifiability/IteratorPrecisionTest.java new file mode 100644 index 000000000000..014dca4b3169 --- /dev/null +++ b/checker/tests/modifiability/IteratorPrecisionTest.java @@ -0,0 +1,439 @@ +import java.util.AbstractCollection; +import java.util.AbstractSequentialList; +import java.util.AbstractSet; +import java.util.ArrayDeque; +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; +import java.util.LinkedList; +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; +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.IteratorPolyMod; +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 { + + enum TestEnum { + A, + B + } + + @IteratorPolyMod List list; + + void iteratorChecker() { + // this should not be allowed because CopyOnWriteArrayList's iterator does not preserve remove + // :: error: [assignment] + list = new CopyOnWriteArrayList<>(); + @Shrinkable Iterator iterator = list.iterator(); + } + + void arrayListIterator() { + // ok + ArrayList list = new ArrayList<>(); + @Shrinkable Iterator iterator = list.iterator(); + iterator.remove(); + + // should still be ok + List list2 = new ArrayList<>(); + Iterator iterator2 = list2.iterator(); + iterator2.remove(); + } + + void copyOnWriteArrayListIterator() { + // as expected, + CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); + @Unshrinkable Iterator iterator = list.iterator(); + + // store CopyOnWriteArrayList as a list, the iterator is @MaybeShrinkable. + List list2 = new CopyOnWriteArrayList<>(); + // 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(); + } + + 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); + } + + // 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(); + } + + 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 iterator2 = 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(); + @Shrinkable Iterator iter2 = keys2.iterator(); + } + + void setIteratorPolyMod() { + Set hashSet = new HashSet<>(); + @Shrinkable Iterator hashSetIterator = hashSet.iterator(); + hashSetIterator.remove(); + + Set treeSet = new TreeSet<>(); + @Shrinkable Iterator treeSetIterator = treeSet.iterator(); + treeSetIterator.remove(); + } + + void dequeIteratorPolyMod() { + Deque arrayDeque = new ArrayDeque<>(); + @Shrinkable Iterator arrayDequeIterator = arrayDeque.iterator(); + arrayDequeIterator.remove(); + + Deque linkedList = new LinkedList<>(); + @Shrinkable Iterator linkedListIterator = linkedList.iterator(); + linkedListIterator.remove(); + } + + void queueIteratorPolyMod() { + 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 factoryIteratorPolyMod() { + 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(); + } + + 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(); + } + + 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 + // :: 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(); + } + }; + } + + @Override + public int size() { + return 1; + } + } +} 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/MapModifiableTest.java b/checker/tests/modifiability/MapModifiableTest.java new file mode 100644 index 000000000000..61267fa7b4d9 --- /dev/null +++ b/checker/tests/modifiability/MapModifiableTest.java @@ -0,0 +1,149 @@ +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; +import org.checkerframework.checker.modifiability.qual.MaybeModifiable; +import org.checkerframework.checker.modifiability.qual.Modifiable; +import org.checkerframework.checker.modifiability.qual.Unmodifiable; + +// 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); + 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() { + @Unmodifiable 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)); + @Unmodifiable Set<@Unmodifiable Entry> 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( + @MaybeModifiable 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); + } + + // 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); + } +} 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/PolyModifiableTest.java b/checker/tests/modifiability/PolyModifiableTest.java new file mode 100644 index 000000000000..5cce419f56e3 --- /dev/null +++ b/checker/tests/modifiability/PolyModifiableTest.java @@ -0,0 +1,179 @@ +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; +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; + +/** + * Tests @PolyModifiable, which expands + * 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 five hierarchies. */ + @PolyModifiable @PolyIteratorPolyMod List identity(@PolyModifiable @PolyIteratorPolyMod 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; + } + + /** 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, + @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, + @MaybeModifiable List unknown, + @Unmodifiable List unmod) { + + // ============================================================ + // Identity on @Modifiable (G+Seq+S+R, on types that support all four capabilities) + // ============================================================ + @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) + // ============================================================ + @Growable @Shrinkable List gs1 = identity(gs); // OK + // :: error: [assignment] + @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; Seq=MaybeSeqGrowable, S=MaybeShrinkable) + // ============================================================ + @Growable @Replaceable List gr1 = identity(gr); // OK + // :: error: [assignment] + @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, Seq=MaybeSeqGrowable) + // ============================================================ + @Shrinkable @Replaceable List sr1 = identity(sr); // OK + // :: error: [assignment] + @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; 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: Seq/S/R=Maybe + + // ============================================================ + // Identity on @Shrinkable (S only; G=Maybe, Seq=Maybe, R=Maybe) + // ============================================================ + @Shrinkable List s1 = identity(s); // OK + // :: error: [assignment] + @Growable @Shrinkable List s2 = identity(s); // Error: G=Maybe + // :: error: [assignment] + @Modifiable List s3 = identity(s); // Error + + // ============================================================ + // Identity on @Replaceable (R only; G=Maybe, Seq=Maybe, S=Maybe) + // ============================================================ + @Replaceable List r1 = identity(r); // OK + // :: error: [assignment] + @Growable @Replaceable List r2 = identity(r); // Error: G=Maybe + // :: error: [assignment] + @Modifiable List r3 = identity(r); // Error + + // ============================================================ + // Identity on @MaybeModifiable (all tops) + // ============================================================ + @MaybeModifiable List u1 = identity(unknown); // OK + // :: error: [assignment] + @Modifiable List u2 = identity(unknown); // Error + // :: error: [assignment] + @Growable List u3 = identity(unknown); // Error + + // ============================================================ + // 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. + } + + 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/PolyShrinkableTest.java b/checker/tests/modifiability/PolyShrinkableTest.java new file mode 100644 index 000000000000..d831c0785c24 --- /dev/null +++ b/checker/tests/modifiability/PolyShrinkableTest.java @@ -0,0 +1,57 @@ +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.modifiability.qual.*; + +public class PolyShrinkableTest { + + 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. + @Modifiable Map modMap = new java.util.HashMap<>(); + @Shrinkable Collection modVals = modMap.values(); + // :: error: [assignment] + @Growable Collection modValsG = modMap.values(); // Cannot add + + // 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 + } + + void testMapEntrySet() { + // entrySet(): Set is @PolyShrinkable (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(); + // :: error: [assignment] + @Growable Set> unmodEntriesG = unmodMap.entrySet(); + } +} diff --git a/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java b/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java new file mode 100644 index 000000000000..5a188a999ff9 --- /dev/null +++ b/checker/tests/modifiability/PreservesModifiabilityPolyArgumentTest.java @@ -0,0 +1,50 @@ +import java.util.ArrayList; +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 +// 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); + } + + 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()); + } +} diff --git a/checker/tests/modifiability/PreservesModifiabilityTest.java b/checker/tests/modifiability/PreservesModifiabilityTest.java new file mode 100644 index 000000000000..640721bd6231 --- /dev/null +++ b/checker/tests/modifiability/PreservesModifiabilityTest.java @@ -0,0 +1,98 @@ +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); + } + + // 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); + } + + // 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, + @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); + } + + // 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); + // :: error: [assignment] + @Growable List g = annotatedTwoArguments(growable, growable); + // :: error: [assignment] + @Growable List v = annotatedVarargs(growable, growable); + } +} diff --git a/checker/tests/modifiability/QueueModifiableTest.java b/checker/tests/modifiability/QueueModifiableTest.java new file mode 100644 index 000000000000..a1a1642def2d --- /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(); + } + } +} diff --git a/checker/tests/modifiability/SeqGrowableMapTest.java b/checker/tests/modifiability/SeqGrowableMapTest.java new file mode 100644 index 000000000000..3e944e7f688e --- /dev/null +++ b/checker/tests/modifiability/SeqGrowableMapTest.java @@ -0,0 +1,93 @@ +// @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 { + + // :: 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: [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: [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/SeqGrowableTest.java b/checker/tests/modifiability/SeqGrowableTest.java new file mode 100644 index 000000000000..59dce03a3437 --- /dev/null +++ b/checker/tests/modifiability/SeqGrowableTest.java @@ -0,0 +1,89 @@ +// @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.SortedSet; +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 { + + // :: 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<>(); + @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/SeqGrowableTest2.java b/checker/tests/modifiability/SeqGrowableTest2.java new file mode 100644 index 000000000000..4556f7e30914 --- /dev/null +++ b/checker/tests/modifiability/SeqGrowableTest2.java @@ -0,0 +1,79 @@ +// @below-java21-jdk-skip-test + +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; +import org.checkerframework.checker.modifiability.qual.SeqGrowable; + +public class SeqGrowableTest2 { + + void testSortedSet(SortedSet s) { + // :: error: [method.invocation] + s.addFirst("foo"); + + // :: error: [method.invocation] + s.addLast("bar"); + } + + void testImplementation(TreeSet ts) { + // Since TreeSet implements SortedSet, it inherits the methods. + // We verify that calling them on a concrete implementation also triggers the + // warning. + + // :: error: [method.invocation] + ts.addFirst("foo"); + + // :: error: [method.invocation] + ts.addLast("bar"); + } + + void testSortedMap(SortedMap m) { + // :: error: [method.invocation] + m.putFirst("foo", "bar"); + + // :: error: [method.invocation] + m.putLast("baz", "qux"); + } + + void testImplementation(TreeMap tm) { + // :: error: [method.invocation] + tm.putFirst("foo", "bar"); + + // :: error: [method.invocation] + tm.putLast("baz", "qux"); + } + + void testNavigableMap(NavigableMap m) { + // :: error: [method.invocation] + m.putFirst("foo", "bar"); + + // :: error: [method.invocation] + m.putLast("baz", "qux"); + } + + void testConcurrentNavigableMap(ConcurrentNavigableMap m) { + // :: error: [method.invocation] + m.putFirst("foo", "bar"); + + // :: error: [method.invocation] + m.putLast("baz", "qux"); + } + + void testConcurrentSkipListMap(ConcurrentSkipListMap m) { + // :: error: [method.invocation] + m.putFirst("foo", "bar"); + + // :: error: [method.invocation] + m.putLast("baz", "qux"); + } + + void f( + // :: error: [annotations.on.use] + @SeqGrowable SortedSet s) { + s.addFirst("x"); + } +} diff --git a/checker/tests/modifiability/SetModifiableTest.java b/checker/tests/modifiability/SetModifiableTest.java new file mode 100644 index 000000000000..8fc819d5eb18 --- /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/checker/tests/modifiability/SetRefinementTest.java b/checker/tests/modifiability/SetRefinementTest.java new file mode 100644 index 000000000000..344eaa5a1588 --- /dev/null +++ b/checker/tests/modifiability/SetRefinementTest.java @@ -0,0 +1,120 @@ +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`. + +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. */ + public SetRefinementTest() {} + + // ========================================================== + // Field access as receiver + // ========================================================== + + public SetRefinementTest(List other) { + // items is @Modifiable from the field initializer. + this.items.addAll(other); + } + + public SetRefinementTest(List other, int dummy) { + this.items.set(0, other.get(0)); + this.items.addAll(other); + } + + public SetRefinementTest(List other, boolean dummy) { + 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); + this.items.set(0, other.get(0)); + this.items.addAll(other); + } + + // 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)); + // :: 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); + } + + // 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 with @DoesNotUnrefineReceiver; refinement of arr[0] preserved + arr[0].addAll(other); + } + + // ========================================================== + // `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/checker/tests/modifiability/SetTest.java b/checker/tests/modifiability/SetTest.java new file mode 100644 index 000000000000..9ddea621efb1 --- /dev/null +++ b/checker/tests/modifiability/SetTest.java @@ -0,0 +1,168 @@ +import java.util.Iterator; +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.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; + +/** + * Tests structural capability removal for Set, Queue, Map.Entry, and Iterator. + * + *

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 { + + // ========================================================== + // Set: explicit Replace capability is preserved. + // ========================================================== + + void testSetReplaceRemoved( + @Replaceable Set replaceable, + @Growable @Replaceable Set growReplace, + @Shrinkable @Replaceable Set shrinkReplace, + @Modifiable Set modifiable) { + + @Growable Set g1 = growReplace; // OK: Grow still intact + @Replaceable Set r1 = growReplace; + + @Growable @Shrinkable Set gs1 = modifiable; // OK + // :: error: [assignment] + @Replaceable Set r2 = modifiable; // maybeReplace cannot assign to replaceable. + } + + void testSetGrowShrinkPreserved( + @MaybeGrowable @MaybeShrinkable @MaybeReplaceable Set unknown, + @Growable Set growable, + @Shrinkable Set shrinkable, + @Growable @Shrinkable Set gs) { + + // Top in all hierarchies: accepts everything + @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 + // :: error: [assignment] + @Growable Set g2 = shrinkable; // Error: S only, no G + + // @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 + } + + void testQueueLikeSet( + @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; + } + + // ========================================================== + // Map.Entry: Grow and Shrink capabilities are both removed. + // Effective types only retain the Replace hierarchy annotation. + // ========================================================== + + void testEntryAssignment( + // 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, + // Has Replace bit: retains @Replaceable after G+S removal + Map.@Replaceable Entry replaceable, + Map.@Growable @Replaceable Entry growReplace, + Map.@Shrinkable @Replaceable Entry shrinkReplace, + Map.@Modifiable Entry modifiable, + Map.@Growable @Shrinkable @Replaceable Entry explicit) { + + // Group 1: entries without Replace bit become @MaybeGrowable @MaybeShrinkable @MaybeReplaceable + // (G and S are removed → both become Maybe; R was already Maybe) + 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; + // :: error: [assignment] + Map.@Shrinkable Entry s1 = unknown; + Map.@Growable Entry g2 = gs; // OK + + 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; + // :: error: [assignment] + Map.@Shrinkable @Replaceable Entry sr1 = replaceable; + Map.@Modifiable Entry m1 = replaceable; + // :: error: [assignment] + Map.@Shrinkable @Replaceable Entry sr2 = growReplace; + Map.@Replaceable Entry gr2 = modifiable; + Map.@Modifiable Entry m2 = explicit; + + // 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 + } + + // ========================================================== + // Iterator: Grow and Replace capabilities are both removed. + // Effective types only retain the Shrink hierarchy annotation. + // ========================================================== + + void testIteratorAssignment( + // 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 → @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 @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; + @Replaceable Iterator r1 = growReplace; + // :: error: [assignment] + @Growable @Replaceable Iterator gr1 = replaceable; + + // 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] + @Growable @Shrinkable Iterator gs1 = modifiable; + // :: error: [assignment] + @Shrinkable @Replaceable Iterator sr1 = modifiable; + @Modifiable Iterator m1 = explicit; + // :: error: [assignment] + @Shrinkable Iterator bad1 = growable; // Error: Shrink=Maybe !<: Shrinkable + } +} 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/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java new file mode 100644 index 000000000000..8534033213af --- /dev/null +++ b/checker/tests/modifiability/SubtypeCapabilityPreservationTest.java @@ -0,0 +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.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() { + List list = new ArrayList<>(); + @Modifiable ListIterator listIterator = list.listIterator(); + 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; + // :: error: [assignment] + @Modifiable ListIterator listIterator4 = (ListIterator) iterator2; + } + + void linkedListThroughQueue() { + @Modifiable LinkedList linkedList = new LinkedList<>(); + @Modifiable Queue queue = linkedList; + @Modifiable LinkedList linkedList2 = (LinkedList) queue; + linkedList2.add("added"); + linkedList2.set(0, "replaced"); + linkedList2.remove(); + } + + void arrayListThroughCollection() { + @Modifiable List arrayList = new ArrayList<>(); + @Modifiable Collection collection = arrayList; + @Modifiable List arrayList2 = (ArrayList) collection; + arrayList2.add("added"); + arrayList2.set(0, "replaced"); + arrayList2.remove(0); + } +} 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; + } +} diff --git a/checker/tests/modifiability/UnmodArrayListTest.java b/checker/tests/modifiability/UnmodArrayListTest.java new file mode 100644 index 000000000000..759e45b95479 --- /dev/null +++ b/checker/tests/modifiability/UnmodArrayListTest.java @@ -0,0 +1,30 @@ +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; + +public class UnmodArrayListTest { + + void testUnmodifiableCastEscape() { + ArrayList myList = new ArrayList<>(); + myList.add("hello"); + + // Checker correctly infers @Unmodifiable here. + @Unmodifiable List view = Collections.unmodifiableList(myList); + + ArrayList backToMod = (ArrayList) view; + + // :: error: [method.invocation] + backToMod.add("Boom"); + } + + void test(@Modifiable ArrayList tests) { + tests.add("a"); + } + + void test2(ArrayList tests) { + // :: error: [method.invocation] + tests.add("a"); + } +} diff --git a/checker/tests/modifiability/UnmodParamLocationTest.java b/checker/tests/modifiability/UnmodParamLocationTest.java new file mode 100644 index 000000000000..0e7836901148 --- /dev/null +++ b/checker/tests/modifiability/UnmodParamLocationTest.java @@ -0,0 +1,58 @@ +import java.util.List; +import java.util.function.Consumer; +import org.checkerframework.checker.modifiability.qual.UnmodifiableParam; + +class UnmodParamLocationTest< + // :: error: [unmodparam.location] + @UnmodifiableParam T> { + + // :: error: [unmodparam.location] + @UnmodifiableParam List field; + + // :: error: [unmodparam.location] + List<@UnmodifiableParam List> nestedField; + + UnmodParamLocationTest(@UnmodifiableParam List parameter) {} + + void method(@UnmodifiableParam List parameter) {} + + void nestedParameter(List<@UnmodifiableParam List> parameter) {} + + abstract static class RelevantReceiver implements List { + void receiver(@UnmodifiableParam RelevantReceiver this) {} + } + + // :: error: [unmodparam.location] + @UnmodifiableParam List returnType() { + return null; + } + + @SuppressWarnings("unchecked") + void local(Object object) { + // :: error: [unmodparam.location] + @UnmodifiableParam List local = null; + local = null; + + // :: error: [unmodparam.location] + List cast = (@UnmodifiableParam List) object; + cast = null; + } + + void lambda() { + 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 new file mode 100644 index 000000000000..073a53618243 --- /dev/null +++ b/checker/tests/modifiability/UoeImplementationTest.java @@ -0,0 +1,158 @@ +// 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; +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. + // :: error: [inherited.implementation.uoe] + 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(); + } + } + + // `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() {} + + @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() {} + + 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/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java index c01598810820..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,8 @@ 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; import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; @@ -15,7 +17,7 @@ public class BusyExprStore implements Store { /** A set of busy expression abstract values. */ - private final Set busyExprValueSet; + private final @Growable @Shrinkable Set busyExprValueSet; /** * Create a new BusyExprStore. @@ -23,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(Set busyExprValueSet) { + public BusyExprStore(@Growable @Shrinkable Set busyExprValueSet) { this.busyExprValueSet = busyExprValueSet; } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b5baf854f094..5d8323d1fb5a 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 new `wpi2.sh` script does whole-program inference, but requires buildfile edits. Purity Checker: diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 2be415c98310..959a3d6f7fc4 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -1005,6 +1005,11 @@ \hyperref[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 + \hyperref[modifiability-checker]{Modifiability Checker} to + warn about a possible \ at run time when + modifying a collection + (see \chapterpageref{modifiability-checker}) \end{itemize} diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 6585207ada94..3c47ae5f1726 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -95,6 +95,11 @@ \hyperref[signedness-checker]{Signedness Checker} to ensure unsigned and signed values are not mixed (see \chapterpageref{signedness-checker}) +\item + \hyperref[modifiability-checker]{Modifiability Checker} to + warn about a possible \ at run time when + modifying a collection + (see \chapterpageref{modifiability-checker}) \item \hyperref[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 f309595bff9c..cdfe1b84dffc 100644 --- a/docs/manual/manual.tex +++ b/docs/manual/manual.tex @@ -78,6 +78,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..5aab22c7344a --- /dev/null +++ b/docs/manual/modifiability-checker.tex @@ -0,0 +1,752 @@ +\chapterAndLabel{Modifiability Checker}{modifiability-checker} + +\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. +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 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.) + +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 \. + +If the Modifiability Checker issues no warning, then your program is +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''.) + +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} +javac -processor org.checkerframework.checker.modifiability.ModifiabilityChecker MyFile.java +\end{Verbatim} + +\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: +\begin{itemize} +\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} 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 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. + Not to be confused with sequential grow. + \item \textbf{Sequential Grow}: The sequenced collection can grow by adding new + elements at the beginning or end (\, \, + \, \, \, \, + \, \ldots). + + The Sequential 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 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 + elements with new values at specific locations/keys (\, \, \, + \, \, \, \ldots). +\end{itemize} + +\noindent +Some collections can also be modified through their iterator; see Section~\ref{modifiability-iterator-behavior}. + +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}{lp{0.75\linewidth}} +Capability required & Method families \\ +\<@Growable> & + \, \, \, + \, \, \, \, + timed \, \, \, + \, \, \, + legacy \ insertion methods, and \ \\ +\<@SeqGrowable> & + \, \, \, + \, \, \, \, + \, \, timed \, + timed \, \, \ \\ +\<@Shrinkable> & + \, \, \, + \, \, \, \, \, + \, timed \, \, \, + \, \, \, occurrence-removal methods, + \, conditional \, \, + \, \, \, + and legacy \ removal methods \\ +\<@Replaceable> & + \, \, + \, \, \, + \, \, 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} + +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 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} + \centering + \begin{minipage}{0.85\linewidth} +\begin{verbatim} + Grow hierarchy SeqGrow hierarchy Shrink hierarchy Replace hierarchy Iterator hierarchy + + @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. + 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{\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} @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 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 @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 @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, seq-grow, shrink, or replace operation. + +\item[\refqualclass{checker/modifiability/qual}{MaybeModifiable}] +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, +seq-grow, shrink, or replace operation. + +\item[\refqualclass{checker/modifiability/qual}{UnmodifiableParam}] +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 @PolySeqGrowable @PolyShrinkable @PolyReplaceable} +(see Section~\ref{modifiability-polymorphic}). + +\end{description} + +\noindent +Examples using alias annotations: + +\begin{Verbatim} + @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 +\end{Verbatim} + +\subsectionAndLabel{Qualifiers}{modifiability-qualifiers} + +\subsubsectionAndLabel{The Grow hierarchy}{modifiability-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. +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 \emph{will not} throw \UnsupportedOperationException. + +\item[\refqualclass{checker/modifiability/qual}{Ungrowable}] +Calling grow operations such as \code{add}, \code{addAll}, etc.\ on this +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} + +\subsubsectionAndLabel{The SeqGrow hierarchy}{modifiability-seqgrow-hierarchy} + +\begin{description} +\item[\refqualclass{checker/modifiability/qual}{MaybeSeqGrowable}] +The top qualifier in the SeqGrow hierarchy. +The checker cannot determine whether this collection or map supports sequenced grow +operations. +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}] +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}] +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. +\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. +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 \emph{will not} throw \UnsupportedOperationException. + +\item[\refqualclass{checker/modifiability/qual}{Unshrinkable}] +Calling shrink operations such as \code{remove}, \code{clear}, etc.\ on +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} + +\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. +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 \emph{will not} throw \UnsupportedOperationException. + +\item[\refqualclass{checker/modifiability/qual}{Unreplaceable}] +Calling replace operations such as \code{set}, \code{replaceAll}, etc.\ +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} + +\subsubsectionAndLabel{The Iterator hierarchy}{modifiability-iterator-hierarchy} + +\begin{description} +\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}{IteratorPolyMod}] +This collection's \code{iterator()} is \<@PolyShrinkable>. That is, if +collection \ is \<@Shrinkable>, then \ is also +\<@Shrinkable>. + +\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 CopyOnWriteArrayList 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} + +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). + +\begin{description} + +\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 \<@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 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. + +\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 \<@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. +Use on methods such as \code{Collections.synchronizedList} that +do not change modifiability. + +\end{description} + + +\subsectionAndLabel{Modifiability method annotations}{modifiability-method-annotations} + +The Modifiability Checker supports method annotations that specify method +behavior. + +\begin{description} + +\item[\refqualclass{checker/modifiability/qual}{PreservesModifiability}] +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 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. + +\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 +\<@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, 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. +For example, \ has +\ but not \ or \, so \<@Modifiable Iterator> expands to +\<@MaybeGrowable @MaybeSeqGrowable @Shrinkable @MaybeReplaceable Iterator>. + + +\subsectionAndLabel{Alternative design: use negative capability rather than top qualifier}{modifiability-modifiable-must-be-top} + +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, \ 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 = ...; +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. 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} +\multicolumn{2}{c}{Weakening conditions for +\<@Modifiable>/\<@Unmodifiable>. ``$\backslash$'' means set difference. A +type includes its subtypes.} \\ \hline +& Expands to \<@Maybe*> \\ \hline +Grow +& \ or (\$\backslash$\) \\ +SeqGrow +& not \, \, or \ \\ +Shrink +& \ \\ +Replace +& exact \, any \, (\$\backslash$\), \\ +& or (\$\backslash$\) \\ +\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. + + +\subsectionAndLabel{Example expansions for \<@Modifiable> and \<@Unmodifiable>}{modifiability-modifiable-expansion-examples} + +The following are representative canonical expansions. + +\noindent +\begin{smaller} +\setlength{\tabcolsep}{.33\tabcolsep} +\begin{tabular}{rlcrrrrl} +\<@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> & \ \\ +\<@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> & \ \\ +\<@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} +\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 + +\begin{Verbatim} +@MaybeShrinkable Iterator iterator() +\end{Verbatim} + +However, for some subclasses of \, the signature of +\ is + +\begin{Verbatim} +@PolyShrinkable Iterator iterator(@PolyShrinkable MySubclassOfCollection this) +\end{Verbatim} + +A collection uses the second signature if the collection is +\<@IteratorPolyMod>. The collection uses the first signature if the +collection is \<@MaybeIteratorPolyMod>. + +Here are examples of its use: + +\begin{Verbatim} + @IteratorPolyMod @Shrinkable List list = ...; + @Shrinkable Iterator it = list.iterator(); + it.remove(); // OK + + @IteratorPolyMod @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} + +\sectionAndLabel{Examples of Modifiability Checker warnings}{modifiability-examples} + +\begin{Verbatim} +import java.util.*; +import org.checkerframework.checker.modifiability.qual.*; + +class Demo { + + void basicUsage() { + // @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 @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 + } + + void fineGrainedPermissions(@Growable List g, + @Shrinkable List s, + @Replaceable List r) { + // Growable list allows adding elements + 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.remove("a"); // OK + s.add("a"); // Error: add() requires @Growable + + // Replaceable list allows updating elements + r.set(0, "b"); // OK + r.add("b"); // Error: add() requires @Growable + } + + 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: @MaybeShrinkable (default) !<: @Shrinkable + } + + // @PolyModifiable preserves all four capabilities + @PolyModifiable List wrap(@PolyModifiable List list) { + return list; + } + + 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 @MaybeSeqGrowable and @MaybeReplaceable + } +} +\end{Verbatim} + +% LocalWords: Modifiability modifiability copyOf emptyList emptySet gsr +% LocalWords: emptyMap unmodifiableList unmodifiableSet unmodifiableMap +% 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 Un +% LocalWords: PolyShrinkable PolyGrowable PolyReplaceable SeqGrow Deque +% LocalWords: addLast offerFirst offerLast putFirst putLast unreplaceable seq-growability +% 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 PolyIteratorPolyMod 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 diff --git a/framework/tests/all-systems/DiamondLambda.java b/framework/tests/all-systems/DiamondLambda.java index e66dfceb9104..b364f5e0a1d5 100644 --- a/framework/tests/all-systems/DiamondLambda.java +++ b/framework/tests/all-systems/DiamondLambda.java @@ -1,9 +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) { + void foo(@Modifiable Map> map, String sequence) { Set set = map.computeIfAbsent(sequence, __ -> new HashSet<>()); } } diff --git a/framework/tests/all-systems/GenericsCasts.java b/framework/tests/all-systems/GenericsCasts.java index 11f9f43367d4..2d9110ff4905 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,7 +27,7 @@ void m() { // are currently problematic. // TODO: try to find a problem with skipping this check. class Test { - class Entry extends LinkedList {} + @Modifiable class Entry extends LinkedList {} class Queue { List poll() { diff --git a/framework/tests/all-systems/GenericsEnclosing.java b/framework/tests/all-systems/GenericsEnclosing.java index 62e8ab1bf5f7..0c93935ed0e7 100644 --- a/framework/tests/all-systems/GenericsEnclosing.java +++ b/framework/tests/all-systems/GenericsEnclosing.java @@ -1,4 +1,7 @@ import java.util.TreeMap; +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 @@ -6,7 +9,8 @@ * *

Also see regex/GenericsEnclosing for a test case for the Regex Checker. */ -public class GenericsEnclosing extends TreeMap { +public @Growable @Replaceable @SeqUngrowable class GenericsEnclosing + extends TreeMap { class Inner { void foo() { put("string", "string".toString()); 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); } } diff --git a/framework/tests/all-systems/Issue8053.java b/framework/tests/all-systems/Issue8053.java index f2fea1e8e8a5..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,7 +42,7 @@ interface Getter {} return types.stream().map(t -> wild(t)).collect(Collectors.toList()); } - static final Map>> CACHE = new HashMap<>(); + static final @Modifiable Map>> CACHE = new HashMap<>(); static native

Getter createGetter(Class

clazz, String name); diff --git a/framework/tests/all-systems/RawTypeAssignment.java b/framework/tests/all-systems/RawTypeAssignment.java index 242db2396f18..b42d16dad10f 100644 --- a/framework/tests/all-systems/RawTypeAssignment.java +++ b/framework/tests/all-systems/RawTypeAssignment.java @@ -1,9 +1,10 @@ import java.util.ArrayList; import java.util.Calendar; +import org.checkerframework.checker.modifiability.qual.Modifiable; class Component {} -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 {} 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(); }