package checkers.igj;
import javax.annotation.processing.*;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import checkers.basetype.BaseTypeChecker;
import checkers.igj.quals.*;
import checkers.quals.TypeQualifiers;
import checkers.types.*;
import checkers.types.AnnotatedTypeMirror.AnnotatedDeclaredType;
import checkers.util.*;
/**
* A type-checker plug-in for the IGJ immutability type system that finds (and
* verifies the absence of) undesired side-effect errors.
*
* The IGJ language is a Java language extension that expresses immutability
* constraints, using six annotations: {@link ReadOnly}, {@link Mutable},
* {@link Immutable}, {@link I} -- a polymorphic qualifier, {@link Assignable},
* and {@link AssignsFields}. The language is specified by the FSE 2007 paper.
*
* @checker.framework.manual #igj-checker IGJ Checker
*
*/
@TypeQualifiers({ ReadOnly.class, Mutable.class, Immutable.class, I.class,
AssignsFields.class, IGJBottom.class })
public class IGJChecker extends BaseTypeChecker {
//
// IGJ tries to adhere to the various rules specified by the
// type system and the conventions of the framework, except for two
// things:
// 1. overloading the meaning of BOTTOM_QUAL
// Review the javadoc of #createQualiferHierarchy
//
// 2. Having two qualifiers for a given type in one particular case
// which is that the self type (i.e. type of 'this' identifier) within
// a method with an AssignsFields receiver within I classes, then the self type is
// '@AssignsFields @I EnclosingClass' and they are treated as
// Incomparable. This is useful in the following cases:
//
// a. for method invocability tests, a method with an AssignsFields receiver from within
// a readonly context can be called only via AssignsFields reference
// of 'this'. I cannot be a receiver type, so it doesn't interfere.
//
// b. for assignment, 'this' can be assigned to '@I EnclosingClass'
// reference within such methods (assignment encompasses the escape
// of this when passed to method parameters). Fields and variables
// cannot be AssignsFields, so it's safe.
//
// The design of QualifierHierarchy.isSubtype(Collection, Collection)
// reflect this choice.
//
/** Supported annotations for IGJ. Used for subtyping rules. **/
protected AnnotationMirror READONLY, MUTABLE, IMMUTABLE, I, ASSIGNS_FIELDS, BOTTOM_QUAL;
@Override
public synchronized void init(ProcessingEnvironment env) {
AnnotationUtils annoFactory = AnnotationUtils.getInstance(env);
READONLY = annoFactory.fromClass(ReadOnly.class);
MUTABLE = annoFactory.fromClass(Mutable.class);
IMMUTABLE = annoFactory.fromClass(Immutable.class);
I = annoFactory.fromClass(I.class);
ASSIGNS_FIELDS = annoFactory.fromClass(AssignsFields.class);
BOTTOM_QUAL = annoFactory.fromClass(IGJBottom.class);
super.init(env);
}
// **********************************************************************
// IGJ specific Type Relationship
// **********************************************************************
/**
* Return true if the assignment variable is an assignable field or
* variable, and returns false otherwise.
*
* A field is assignable if it is
*
* 1. a static field
* 2. marked {@link Assignable}
* 3. accessed through a mutable reference
* 4. reassigned with an {@link AssignsFields} method and owned by 'this'
*
*/
@Override
public boolean isAssignable(AnnotatedTypeMirror varType,
AnnotatedTypeMirror receiverType, Tree varTree) {
if (!(varTree instanceof ExpressionTree))
return true;
Element varElement = InternalUtils.symbol(varTree);
if (varTree.getKind() != Tree.Kind.ARRAY_ACCESS
&& (varElement == null // a variable element should never be null
|| !varElement.getKind().isField()
|| ElementUtils.isStatic(varElement)
|| varElement.getAnnotation(Assignable.class) != null))
return true;
assert receiverType != null;
final boolean isAssignable =
(receiverType.hasAnnotation(MUTABLE)
|| receiverType.hasAnnotation(BOTTOM_QUAL)
|| (receiverType.hasAnnotation(ASSIGNS_FIELDS)
&& TreeUtils.isSelfAccess((ExpressionTree)varTree)));
return isAssignable;
}
@Override
public boolean isValidUse(AnnotatedDeclaredType elemType, AnnotatedDeclaredType use) {
if (elemType.hasAnnotation(I) || use.hasAnnotation(READONLY))
return true;
return super.isValidUse(elemType, use);
}
// **********************************************************************
// Factory methods
// **********************************************************************
@Override
protected QualifierHierarchy createQualifierHierarchy() {
return new IGJQualifierHierarchy((GraphQualifierHierarchy)super.createQualifierHierarchy());
}
@Override
protected TypeHierarchy createTypeHierarchy() {
return new IGJTypeHierarchy(getQualifierHierarchy());
}
//
// IGJ makes an interesting use of BOTTOM_QUAL (IGJBottom). It gets used
// in two ways:
//
// __ AS BOTTOM QUALIFIER __
//
// This is the intended use and design for it
//
// A bottom qualifier is needed to annotate some expressions, like 'null'.
// Otherwise, null would need to be '@Mutable @Immutable <nulltype>' so
// it could be assigned to everything.
//
// __ AS SUPER QUALIFIER __
//
// As I used IGJ, I realized that it's useful to have an annotation
// that acts as a place holder qualifier that is a supertype of
// everything. The semantics of such qualifier is a bit
// different from ReadOnly.
//
// It's only because the existence of un-annotated code that we need this
// annotation, and IGJ's promise that un-annotated code should type check.
//
// TODO: Explain these cases more
//
private final class IGJQualifierHierarchy extends GraphQualifierHierarchy {
public IGJQualifierHierarchy(GraphQualifierHierarchy hierarchy) {
super(hierarchy);
}
public boolean isSubtype(AnnotationMirror rhs, AnnotationMirror lhs) {
return (AnnotationUtils.areSame(rhs, BOTTOM_QUAL)
|| AnnotationUtils.areSame(lhs, BOTTOM_QUAL)
|| super.isSubtype(rhs, lhs));
}
}
/**
* Represents the annotated type hierarchy of the IGJ type system.
*
* The IGJ type system diverges from the JLS in two ways:
* 1. Type arguments are always co-variant with respect to
* {@link IGJBottom}
*
* 2. If the type is a read-only or an immutable type, then type arguments
* may change co-variantly in a safe manner
*/
private final class IGJTypeHierarchy extends TypeHierarchy {
public IGJTypeHierarchy(QualifierHierarchy qualifierHierarchy) {
super(qualifierHierarchy);
}
/**
* Returns true if either of the provided types is a
* {@link IGJBottom}, otherwise uses the JLS specification
* implemented by the abstract {@link typeHierarchy}.
*
*/
// Note: This cannot be expressed with the QualifierHierarchy alone,
// as TypeHierarchy requires type arguments to be equivalent
@Override
protected boolean isSubtypeAsTypeArgument(AnnotatedTypeMirror rhs, AnnotatedTypeMirror lhs) {
return (lhs.hasAnnotation(BOTTOM_QUAL)
|| rhs.hasAnnotation(BOTTOM_QUAL)
|| super.isSubtypeAsTypeArgument(rhs, lhs));
}
/**
* Uses the JLS specification (as implemented in {@link TypeHierarchy},
* if the variable type, lhs, is mutable; otherwise, allows the type
* arguments to change while maintaining subtype relationship.
*
* This allows for subtyping relationships of the kind:
* <pre> @Mutable List<@Mutable Date> <: @ReadOnly List<@ReadOnly Date><\pre>
*/
@Override
protected boolean isSubtypeTypeArguments(AnnotatedDeclaredType rhs, AnnotatedDeclaredType lhs) {
if (lhs.hasAnnotation(MUTABLE))
return super.isSubtypeTypeArguments(rhs, lhs);
if (!lhs.getTypeArguments().isEmpty()
&& !rhs.getTypeArguments().isEmpty()) {
assert lhs.getTypeArguments().size() == rhs.getTypeArguments().size();
for (int i = 0; i < lhs.getTypeArguments().size(); ++i) {
if (!isSubtype(rhs.getTypeArguments().get(i), lhs.getTypeArguments().get(i)))
return false;
}
}
return true;
}
}
}