package checkers.igj;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import com.sun.source.tree.*;
import checkers.igj.quals.*;
import checkers.types.*;
import checkers.types.AnnotatedTypeMirror.*;
import checkers.types.visitors.AnnotatedTypeScanner;
import checkers.types.visitors.SimpleAnnotatedTypeVisitor;
import checkers.util.*;
/**
* Adds implicit and default IGJ annotations, only if the user does not
* annotate the type explicitly. The default annotations are designed
* to minimize the number of {@code Immutable} or {@code ReadOnly}
* appearing in the source code.
* <p>
*
* Implicit Annotations for literals:<br/>
* Immutable - any primitive literal (e.g. integer, long, boolean, etc.)<br/>
* IGJBottom - a null literal
* <p>
*
* However, due to the default setting being similar to the implicit
* annotations, there is no significant distinction between the two in
* implementation.
* <p>
*
* Default Annotations:
* <p>
*
* This factory will add the {@link Immutable} annotation to a type if the
* input is
* <ol>
* <li value="1">(*)a primitive type,
* <li value="2">a known immutable type, if the class type is annotated as
* {@code Immutable}
* </ol>
*
* It will add the {@link ReadOnly} annotation to a type if the input is
* <ol>
* <li value="3">a method receiver for an immutable class
* <li value="4">a result of unification of different immutabilities (e.g.
* within Conditional Expressions)
* <li value="5">supertype of a wildcard/type parameter in a class/method declaration
* </ol>
*
* It will add {@link IGJBottom}, a special bottom annotation to a type if
* the input can be assigned to anything, like the following cases:
* <ol>
* <li value="6">(*)the input is a {@code null} literal
* <li value="7">(*)the input is an unannotated new array tree
* <li value="8">the input is an unannotated new class tree invoking a constructor
* of {@code ReadOnly} or {@code AssignsFields} receiver type
* <li value="9">the input is the class or interface declaration
* </ol>
*
* It will add the {@link Mutable} annotation to a type if
* <ol>
* <li value="10">any remaining unqualified types (i.e. Mutable is the default)
* </ol>
*
* Implementation detail: (*) cases are handled with a meta annotation
* rather than in this class.
* <p>
*
* Furthermore, it resolves {@link I} annotation to the proper annotation,
* according to its specification (described in {@link I} javadoc).
*/
//
// To ease dealing with libraries, this inserts the bottom qualifier
// rather than immutable in many cases, like all literals.
// Should change that
public class IGJAnnotatedTypeFactory extends BasicAnnotatedTypeFactory<IGJChecker> {
static { FLOW_BY_DEFAULT = true; }
/** The various IGJ annotations. */
private final AnnotationMirror READONLY, MUTABLE, IMMUTABLE, I,
BOTTOM_QUAL, ASSIGNS_FIELDS;
/** the {@link I} annotation value key */
protected static final String IMMUTABILITY_KEY = "value";
/**
* Constructor for IGJAnnotatedTypeFactory object.
*
* @param checker the checker to which this factory belongs
* @param root the compilation unit the annotation processor is
* processing currently
*/
public IGJAnnotatedTypeFactory(IGJChecker checker,
CompilationUnitTree root) {
super(checker, root);
READONLY = checker.READONLY;
MUTABLE = checker.MUTABLE;
IMMUTABLE = checker.IMMUTABLE;
I = checker.I;
BOTTOM_QUAL = checker.BOTTOM_QUAL;
ASSIGNS_FIELDS = checker.ASSIGNS_FIELDS;
}
@Override
protected Set<AnnotationMirror> createFlowQualifiers(IGJChecker checker) {
AnnotationUtils annoFactory = AnnotationUtils.getInstance(env);
Set<AnnotationMirror> flowQuals = new HashSet<AnnotationMirror>();
for (Class<? extends Annotation> cl : checker.getSupportedTypeQualifiers()) {
if (!I.class.equals(cl))
flowQuals.add(annoFactory.fromClass(cl));
}
return flowQuals;
}
@Override
protected TreeAnnotator createTreeAnnotator(IGJChecker checker) {
return new IGJTreePreAnnotator(checker);
}
@Override
protected TypeAnnotator createTypeAnnotator(IGJChecker checker) {
return new IGJTypePostAnnotator(checker);
}
// **********************************************************************
// add implicit annotations
// **********************************************************************
/**
* Helper class for annotating unannotated types.
*/
private class IGJTypePostAnnotator extends TypeAnnotator {
public IGJTypePostAnnotator(IGJChecker checker) {
super(checker);
}
/**
* For Declared types:
* Classes are mutable
* Interface declaration are placeholders
* Enum and annotations are immutable
*/
@Override
public Void visitDeclared(AnnotatedDeclaredType type, ElementKind p) {
if (!hasImmutabilityAnnotation(type)) {
// Actual element
TypeElement element = (TypeElement)type.getUnderlyingType().asElement();
AnnotatedDeclaredType elementType = fromElement(element);
if (TypesUtils.isBoxedPrimitive(type.getUnderlyingType())
|| element.getQualifiedName().contentEquals("java.lang.String")
|| ElementUtils.isObject(element)) {
// variation of case 1
// TODO: These cases are more of hacks and they should
// really be immutable or readonly
type.addAnnotation(BOTTOM_QUAL);
} else if (elementType.hasAnnotation(IMMUTABLE))
// case 2: known immutable types
type.addAnnotation(IMMUTABLE);
else if (p == ElementKind.LOCAL_VARIABLE)
type.addAnnotation(READONLY);
else if (elementType.hasAnnotation(MUTABLE)) // not immutable
// case 7: mutable by default
type.addAnnotation(MUTABLE);
else if (p.isClass() || p.isInterface())
// case 9: class or interface declaration
type.addAnnotation(BOTTOM_QUAL);
else if (p.isField()
&& type.getElement() != null // We don't know the field context here
&& getAnnotatedType(ElementUtils.enclosingClass(type.getElement())).hasAnnotation(IMMUTABLE)) {
type.addAnnotation(IMMUTABLE);
}
else if (element.getKind().isClass() || element.getKind().isInterface())
// case 10
type.addAnnotation(MUTABLE);
else
assert false : "shouldn't be here!";
}
return super.visitDeclared(type,
p == ElementKind.LOCAL_VARIABLE || p == ElementKind.FIELD ? ElementKind.OTHER : p);
}
@Override
public Void visitExecutable(AnnotatedExecutableType type, ElementKind p) {
if (hasImmutabilityAnnotation(type.getReceiverType()))
return super.visitExecutable(type, p);
AnnotatedDeclaredType receiver = type.getReceiverType();
TypeElement ownerElement = ElementUtils.enclosingClass(type.getElement());
AnnotatedDeclaredType ownerType = getAnnotatedType(ownerElement);
if (type.getElement().getKind() == ElementKind.CONSTRUCTOR) {
// TODO: hack
if (ownerType.hasAnnotation(MUTABLE) || ownerType.hasAnnotation(BOTTOM_QUAL))
receiver.addAnnotation(MUTABLE);
else
receiver.addAnnotation(ASSIGNS_FIELDS);
} else if (ElementUtils.isObject(ownerElement) || ownerType.hasAnnotation(IMMUTABLE)) {
// case 3
receiver.addAnnotation(BOTTOM_QUAL);
} else {
// case 10: rest
receiver.addAnnotation(MUTABLE);
}
return super.visitExecutable(type, p);
}
@Override
public Void visitTypeVariable(AnnotatedTypeVariable type, ElementKind p) {
// In a declaration the upperbound is ReadOnly, while
// the upper bound in a use is Mutable
if (type.getUpperBound() != null
&& !hasImmutabilityAnnotation(type.getUpperBound())) {
if (p.isClass() || p.isInterface()
|| p == ElementKind.CONSTRUCTOR
|| p == ElementKind.METHOD)
// case 5: upper bound within a class/method declaration
type.getUpperBound().addAnnotation(READONLY);
else if (TypesUtils.isObject(type.getUnderlyingType()))
// case 10: remaining cases
type.getUpperBound().addAnnotation(MUTABLE);
}
return super.visitTypeVariable(type, p);
}
@Override
public Void visitWildcard(AnnotatedWildcardType type, ElementKind p) {
// In a declaration the upper bound is ReadOnly, while
// the upper bound in a use is Mutable
if (type.getExtendsBound() != null
&& !hasImmutabilityAnnotation(type.getExtendsBound())) {
if (p.isClass() || p.isInterface()
|| p == ElementKind.CONSTRUCTOR
|| p == ElementKind.METHOD)
// case 5: upper bound within a class/method declaration
type.getExtendsBound().addAnnotation(READONLY);
else if (TypesUtils.isObject(type.getUnderlyingType()))
// case 10: remaining cases
type.getExtendsBound().addAnnotation(MUTABLE);
}
return super.visitWildcard(type, p);
}
}
/**
* Helper class to annotate trees.
*
*
* It only adds an BOTTOM_QUAL for new classes and new arrays,
* when an annotation is not specified
*/
private class IGJTreePreAnnotator extends TreeAnnotator {
public IGJTreePreAnnotator(IGJChecker checker) {
super(checker, IGJAnnotatedTypeFactory.this);
}
@Override
public Void visitNewClass(NewClassTree node, AnnotatedTypeMirror p) {
if (!hasImmutabilityAnnotation(p)) {
AnnotatedTypeMirror ct = fromElement(
((AnnotatedDeclaredType)p).getUnderlyingType().asElement());
if (!hasImmutabilityAnnotation(ct) || ct.hasAnnotation(I)) {
AnnotatedExecutableType con = getAnnotatedType(TreeUtils.elementFromUse(node));
if (con.getReceiverType().hasAnnotation(IMMUTABLE))
p.addAnnotation(IMMUTABLE);
else
p.addAnnotation(MUTABLE);
} else {
// case 2: known immutability type
p.addAnnotations(ct.getAnnotations());
}
}
return null;
}
@Override
public Void visitTypeCast(TypeCastTree node, AnnotatedTypeMirror p) {
if (!hasImmutabilityAnnotation(p)) {
AnnotatedTypeMirror castedType = getAnnotatedType(node.getExpression());
p.addAnnotations(castedType.getAnnotations());
}
return null;
}
}
@Override
protected AnnotatedDeclaredType getImplicitReceiverType(Tree tree) {
AnnotatedDeclaredType receiver = super.getImplicitReceiverType(tree);
if (receiver != null && !isMostEnclosingThisDeref(tree)) {
receiver.removeAnnotation(ASSIGNS_FIELDS);
receiver.addAnnotation(READONLY);
}
return receiver;
}
/**
* Returns the type of field {@code this}, for the scope of this tree.
* In IGJ, the self type is the method receiver in this scope.
*/
@Override
public AnnotatedDeclaredType getSelfType(Tree tree) {
AnnotatedDeclaredType act = getCurrentClassType(tree);
AnnotatedDeclaredType methodReceiver = getCurrentMethodReceiver(tree);
if (methodReceiver == null)
return act;
// Are we in a mutable or Immutable scope
if (isWithinConstructor(tree) && !methodReceiver.hasAnnotation(MUTABLE)) {
methodReceiver.clearAnnotations();
methodReceiver.addAnnotation(ASSIGNS_FIELDS);
}
if (methodReceiver.hasAnnotation(MUTABLE) ||
methodReceiver.hasAnnotation(IMMUTABLE)) {
return methodReceiver;
} else if (act.hasAnnotation(I) || act.hasAnnotation(IMMUTABLE)) {
if (methodReceiver.hasAnnotation(ASSIGNS_FIELDS))
act.addAnnotation(ASSIGNS_FIELDS);
return act;
} else
return methodReceiver;
}
// **********************************************************************
// resolving @I Immutability
// **********************************************************************
/**
* Replace all instances of {@code @I} in the super types with the
* immutability of the current type
*
* @param type the type whose supertypes are requested
* @param supertypes the supertypes of type
*/
@Override
protected void postDirectSuperTypes(AnnotatedTypeMirror type,
List<? extends AnnotatedTypeMirror> supertypes) {
super.postDirectSuperTypes(type, supertypes);
Map<String, AnnotationMirror> templateMapping =
new ImmutabilityTemplateCollector().visit(type);
new ImmutabilityResolver().visit(supertypes, templateMapping);
for (AnnotatedTypeMirror supertype: supertypes)
typeAnnotator.visit(supertype, ElementKind.OTHER);
}
/**
* Resolve the instances of {@code @I} in the {@code elementType} based
* on {@code owner}, according to is specification.
*/
@Override
public void postAsMemberOf(AnnotatedTypeMirror elementType,
AnnotatedTypeMirror owner, Element element) {
resolveImmutabilityTypeVar(elementType, owner);
}
/**
* Resolves {@code @I} in the type of the method type base on the method
* invocation tree parameters. Any unresolved {@code @I}s is resolved to a
* place holder type.
*
* It resolves {@code @I} annotation in the following way:
* <ul>
* <li>based on the tree receiver, done automatically through implicit
* invocation of
* {@link AnnotatedTypes#asMemberOf(AnnotatedTypeMirror, Element)}</li>
* <li>based on the invocation passed parameters</li>
* <li>if any yet unresolved immutability variables get resolved to a
* wildcard type</li>
* </ul>
*/
@Override
public AnnotatedExecutableType methodFromUse(MethodInvocationTree tree) {
AnnotatedExecutableType type = super.methodFromUse(tree);
List<AnnotatedTypeMirror> arguments = atypes.getAnnotatedTypes(tree.getArguments());
List<AnnotatedTypeMirror> requiredArgs = atypes.expandVarArgs(type, tree.getArguments());
ImmutabilityTemplateCollector collector = new ImmutabilityTemplateCollector();
Map<String, AnnotationMirror> matchingMapping = collector.visit(arguments, requiredArgs);
if (!matchingMapping.isEmpty())
new ImmutabilityResolver().visit(type, matchingMapping);
// For finding resolved types, rather than to actually resolve immutability
Map<String, AnnotationMirror> fromReceiver = collector.visit(getReceiver(tree));
final Map<String, AnnotationMirror> mapping =
collector.reduce(matchingMapping, fromReceiver);
new AnnotatedTypeScanner<Void, Void>() {
@Override
public Void visitDeclared(AnnotatedDeclaredType type, Void p) {
if (type.hasAnnotation(I)) {
AnnotationMirror anno =
type.getAnnotation(I.class.getCanonicalName());
if (!mapping.containsValue(anno)) {
type.removeAnnotation(I);
type.addAnnotation(BOTTOM_QUAL);
}
}
return super.visitDeclared(type, p);
}
}.visit(type);
return type;
}
/**
* Infers the immutability of {@code @I}s based on the provided types, and
* replace all instances of {@code @I} with their corresponding qualifiers.
* The {@code @I} annotations that are not resolved are left intact.
*
* @param type the type with {@code @I} annotation
* @param provided the types with qualifiers that may be bound to
* {@code @I}
* @return true iff a qualifier has been resolved.
*/
private boolean resolveImmutabilityTypeVar(AnnotatedTypeMirror type,
AnnotatedTypeMirror ...provided) {
ImmutabilityTemplateCollector collector = new ImmutabilityTemplateCollector();
// maps the @I values to I resolved annotations
Map<String, AnnotationMirror> templateMapping = Collections.emptyMap();
for (AnnotatedTypeMirror pt : provided)
templateMapping = collector.reduce(templateMapping, collector.visit(pt));
// There is nothing to resolve
if (templateMapping.isEmpty())
return false;
new ImmutabilityResolver().visit(type, templateMapping);
return true;
}
/**
* A helper class that resolves the immutability on a types based on a
* provided mapping.
*
* It returns a set of the annotations that were inserted. This is important
* to recognize which immutability type variables were resolved and which
* are to be made into place holder.
*/
private class ImmutabilityResolver extends
AnnotatedTypeScanner<Void, Map<String, AnnotationMirror>> {
public void visit(Iterable<? extends AnnotatedTypeMirror> types,
Map<String, AnnotationMirror> templateMapping) {
if (templateMapping != null && !templateMapping.isEmpty()) {
for (AnnotatedTypeMirror type : types)
visit(type, templateMapping);
}
}
@Override
public Void visitDeclared(AnnotatedDeclaredType type,
Map<String, AnnotationMirror> p) {
if (type.hasAnnotation(I)) {
String immutableString =
AnnotationUtils.parseStringValue(getImmutabilityAnnotation(type),
IMMUTABILITY_KEY);
if (p.containsKey(immutableString)) {
type.removeAnnotation(I);
type.addAnnotation(p.get(immutableString));
}
}
return super.visitDeclared(type, p);
}
}
/**
* A Helper class that tries to resolve the immutability type variable,
* as the type variable is assigned to the most restricted immutability
*/
private class ImmutabilityTemplateCollector
extends SimpleAnnotatedTypeVisitor<Map<String, AnnotationMirror>, AnnotatedTypeMirror> {
public Map<String, AnnotationMirror> reduce(Map<String, AnnotationMirror> r1,
Map<String, AnnotationMirror> r2) {
Map<String, AnnotationMirror> result =
new HashMap<String, AnnotationMirror>();
if (r1 != null)
result.putAll(r1);
if (r2 != null) {
// Need to be careful about overlap
for (String key : r2.keySet()) {
if (!result.containsKey(key))
result.put(key, r2.get(key));
else if (!AnnotationUtils.areSame(result.get(key), r2.get(key)))
result.put(key, READONLY);
}
}
return result;
}
public Map<String, AnnotationMirror> visit(Iterable<? extends AnnotatedTypeMirror> types,
Iterable<? extends AnnotatedTypeMirror> actualTypes) {
Map<String, AnnotationMirror> result = new HashMap<String, AnnotationMirror>();
Iterator<? extends AnnotatedTypeMirror> itert = types.iterator();
Iterator<? extends AnnotatedTypeMirror> itera = actualTypes.iterator();
while (itert.hasNext() && itera.hasNext()) {
AnnotatedTypeMirror type = itert.next();
AnnotatedTypeMirror actualType = itera.next();
result = reduce(result, visit(type, actualType));
}
return result;
}
@Override
public Map<String, AnnotationMirror> visitDeclared(
AnnotatedDeclaredType type, AnnotatedTypeMirror actualType) {
if (actualType == null) {
TypeElement elem = (TypeElement)type.getUnderlyingType().asElement();
actualType = getAnnotatedType(elem);
}
if (actualType.getKind() == TypeKind.TYPEVAR) {
if (typeVar.contains(actualType.getUnderlyingType()))
return Collections.emptyMap();
typeVar.add((TypeVariable)actualType.getUnderlyingType());
Map<String, AnnotationMirror> result = visit(type, ((AnnotatedTypeVariable)actualType).getUpperBound());
typeVar.remove(actualType.getUnderlyingType());
return result;
}
if (actualType.getKind() == TypeKind.WILDCARD)
return visit(type, ((AnnotatedWildcardType)actualType).getExtendsBound());
if (actualType.getKind() != type.getKind())
return Collections.emptyMap();
assert actualType.getKind() == type.getKind();
type = (AnnotatedDeclaredType)atypes.asSuper(type, actualType);
if (type == null)
return Collections.emptyMap();
AnnotatedDeclaredType dcType = (AnnotatedDeclaredType)actualType;
Map<String, AnnotationMirror> result =
new HashMap<String, AnnotationMirror>();
if (dcType.hasAnnotation(I)) {
String immutableString =
AnnotationUtils.parseStringValue(getImmutabilityAnnotation(dcType),
IMMUTABILITY_KEY);
AnnotationMirror immutability = getImmutabilityAnnotation(type);
// Assertion failes some times
assert immutability != null;
if (!immutability.equals(ASSIGNS_FIELDS))
result.put(immutableString, immutability);
}
if (type != dcType && type.isParameterized() && dcType.isParameterized())
result = reduce(result, visit(type.getTypeArguments(), dcType.getTypeArguments()));
return result;
}
@Override
public Map<String, AnnotationMirror> visitArray(
AnnotatedArrayType type, AnnotatedTypeMirror actualType) {
if (actualType == null)
return visit(type.getComponentType(), null);
if (actualType.getKind() == TypeKind.DECLARED)
return visit(atypes.asSuper(type, actualType), actualType);
if (actualType.getKind() == TypeKind.TYPEVAR) {
if (typeVar.contains(actualType.getUnderlyingType()))
return Collections.emptyMap();
typeVar.add((TypeVariable)actualType.getUnderlyingType());
Map<String, AnnotationMirror> result = visit(type, ((AnnotatedTypeVariable)actualType).getUpperBound());
typeVar.remove(actualType.getUnderlyingType());
return result;
}
if (actualType.getKind() == TypeKind.WILDCARD)
return visit(type, ((AnnotatedWildcardType)actualType).getExtendsBound());
if (type.getKind() != actualType.getKind())
return visit(type, null);
assert type.getKind() == actualType.getKind();
AnnotatedArrayType arType = (AnnotatedArrayType)actualType;
Map<String, AnnotationMirror> result =
new HashMap<String, AnnotationMirror>();
if (arType.hasAnnotation(I)) {
String immutableString =
AnnotationUtils.parseStringValue(getImmutabilityAnnotation(arType),
IMMUTABILITY_KEY);
AnnotationMirror immutability = getImmutabilityAnnotation(type);
// Assertion failes some times
assert immutability != null;
if (!type.hasAnnotation(ASSIGNS_FIELDS))
result.put(immutableString, immutability);
}
result = reduce(result, visit(type.getComponentType(), arType.getComponentType()));
return result;
}
private Set<TypeVariable> typeVar = new HashSet<TypeVariable>();
@Override
public Map<String, AnnotationMirror> visitTypeVariable(
AnnotatedTypeVariable type, AnnotatedTypeMirror actualType) {
if (actualType == null)
return Collections.emptyMap();
if (actualType.getKind() == TypeKind.WILDCARD
&& ((AnnotatedWildcardType)actualType).getSuperBound() != null)
actualType = ((AnnotatedWildcardType)actualType).getSuperBound();
AnnotatedTypeMirror typeSuper = findType(type, actualType);
if (typeSuper.getKind() != TypeKind.TYPEVAR)
return visit(typeSuper, actualType);
assert typeSuper.getKind() == actualType.getKind() : actualType;
assert type.getKind() == actualType.getKind() : actualType;
AnnotatedTypeVariable tvType = (AnnotatedTypeVariable)typeSuper;
typeVar.add(type.getUnderlyingType());
// a type variable cannot be annotated
Map<String, AnnotationMirror> result = visit(type.getUpperBound(), tvType.getUpperBound());
typeVar.remove(type.getUnderlyingType());
return result;
}
@Override
public Map<String, AnnotationMirror> visitWildcard(
AnnotatedWildcardType type, AnnotatedTypeMirror actualType) {
if (actualType == null)
return Collections.emptyMap();
AnnotatedTypeMirror typeSuper = findType(type, actualType);
if (typeSuper.getKind() != TypeKind.WILDCARD)
return visit(typeSuper, actualType);
// TODO: Fix this
if (typeSuper.getKind() != actualType.getKind())
return Collections.emptyMap();
assert typeSuper.getKind() == actualType.getKind() : actualType;
AnnotatedWildcardType wcType = (AnnotatedWildcardType)typeSuper;
if (type.getExtendsBound() != null && wcType.getExtendsBound() != null)
return visit(type.getExtendsBound(), wcType.getExtendsBound());
else if (type.getSuperBound() != null && wcType.getSuperBound() != null)
return visit(type.getSuperBound(), wcType.getSuperBound());
else
return new HashMap<String, AnnotationMirror>();
}
private AnnotatedTypeMirror findType(AnnotatedTypeMirror type, AnnotatedTypeMirror actualType) {
AnnotatedTypeMirror result = atypes.asSuper(type, actualType);
// result shouldn't be null, will test this hypothesis later
// assert result != null;
return (result != null ? result : type);
}
}
// **********************************************************************
// Random utility methods
// **********************************************************************
/**
* Returns the annotation specifying the immutability type of {@code type}.
*/
private AnnotationMirror getImmutabilityAnnotation(AnnotatedTypeMirror type) {
// @I and @AssignsFields annotate the type of 'this' together
// this one ensures that it returns @I
//
if (type.hasAnnotation(I))
return type.getAnnotation(I.class.getCanonicalName());
return type.getAnnotations().iterator().next();
}
/**
* @return true iff the type has an immutability qualifier,
* false otherwise
*/
private boolean hasImmutabilityAnnotation(AnnotatedTypeMirror type) {
return type.isAnnotated();
}
}