blob: 3bb9e77999bd525d17784c3ae0dadae905131a85 [file] [log] [blame]
package org.checkerframework.framework.type.typeannotator;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultFor;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.TypesUtils;
/**
* Adds annotations to a type based on the use of a type. This class applies annotations specified
* by {@link DefaultFor}; it is designed to be used in a {@link ListTypeAnnotator} constructed in
* {@link GenericAnnotatedTypeFactory#createTypeAnnotator()} ()}
*
* <p>{@link DefaultForTypeAnnotator} traverses types deeply.
*
* <p>This class takes care of two of the attributes of {@link DefaultFor}; the others are handled
* in {@link org.checkerframework.framework.util.defaults.QualifierDefaults}.
*
* @see ListTypeAnnotator
*/
public class DefaultForTypeAnnotator extends TypeAnnotator {
/** Map from {@link TypeKind} to annotations. */
private final Map<TypeKind, Set<AnnotationMirror>> typeKinds;
/** Map from {@link AnnotatedTypeMirror} classes to annotations. */
private final Map<Class<? extends AnnotatedTypeMirror>, Set<AnnotationMirror>> atmClasses;
/** Map from fully qualified class name strings to annotations. */
private final Map<String, Set<AnnotationMirror>> types;
/**
* A list where each element associates an annotation with name regexes and name exception
* regexes.
*/
private final ListOfNameRegexes listOfNameRegexes;
/** {@link QualifierHierarchy} */
private final QualifierHierarchy qualHierarchy;
/**
* Creates a {@link DefaultForTypeAnnotator} from the given checker, using that checker to
* determine the annotations that are in the type hierarchy.
*/
public DefaultForTypeAnnotator(AnnotatedTypeFactory typeFactory) {
super(typeFactory);
this.typeKinds = new EnumMap<>(TypeKind.class);
this.atmClasses = new HashMap<>();
this.types = new HashMap<>();
this.listOfNameRegexes = new ListOfNameRegexes();
this.qualHierarchy = typeFactory.getQualifierHierarchy();
// Get type qualifiers from the checker.
Set<Class<? extends Annotation>> quals = typeFactory.getSupportedTypeQualifiers();
// For each qualifier, read the @DefaultFor annotation and put its types, kinds, and names
// into maps.
for (Class<? extends Annotation> qual : quals) {
DefaultFor defaultFor = qual.getAnnotation(DefaultFor.class);
if (defaultFor == null) {
continue;
}
AnnotationMirror theQual = AnnotationBuilder.fromClass(typeFactory.getElementUtils(), qual);
for (org.checkerframework.framework.qual.TypeKind typeKind : defaultFor.typeKinds()) {
TypeKind mappedTk = mapTypeKinds(typeKind);
addTypeKind(mappedTk, theQual);
}
for (Class<?> typeName : defaultFor.types()) {
addTypes(typeName, theQual);
}
listOfNameRegexes.add(theQual, defaultFor);
}
}
/**
* Map between {@link org.checkerframework.framework.qual.TypeKind} and {@link
* javax.lang.model.type.TypeKind}.
*
* @param typeKind the Checker Framework TypeKind
* @return the javax TypeKind
*/
private TypeKind mapTypeKinds(org.checkerframework.framework.qual.TypeKind typeKind) {
return TypeKind.valueOf(typeKind.name());
}
/** Add default qualifier, {@code theQual}, for the given TypeKind. */
public void addTypeKind(TypeKind typeKind, AnnotationMirror theQual) {
boolean res = qualHierarchy.updateMappingToMutableSet(typeKinds, typeKind, theQual);
if (!res) {
throw new BugInCF(
"TypeAnnotator: invalid update of typeKinds "
+ typeKinds
+ " at "
+ typeKind
+ " with "
+ theQual);
}
}
/** Add default qualifier, {@code theQual}, for the given {@link AnnotatedTypeMirror} class. */
public void addAtmClass(
Class<? extends AnnotatedTypeMirror> typeClass, AnnotationMirror theQual) {
boolean res = qualHierarchy.updateMappingToMutableSet(atmClasses, typeClass, theQual);
if (!res) {
throw new BugInCF(
"TypeAnnotator: invalid update of atmClasses "
+ atmClasses
+ " at "
+ typeClass
+ " with "
+ theQual);
}
}
/** Add default qualifier, {@code theQual}, for the given type. */
public void addTypes(Class<?> clazz, AnnotationMirror theQual) {
String typeNameString = clazz.getCanonicalName();
boolean res = qualHierarchy.updateMappingToMutableSet(types, typeNameString, theQual);
if (!res) {
throw new BugInCF(
"TypeAnnotator: invalid update of types " + types + " at " + clazz + " with " + theQual);
}
}
@Override
protected Void scan(AnnotatedTypeMirror type, Void p) {
// If the type's fully-qualified name is in the appropriate map, annotate the type. Do this
// before looking at kind or class, as this information is more specific.
String qname;
if (type.getKind() == TypeKind.DECLARED) {
qname = TypesUtils.getQualifiedName((DeclaredType) type.getUnderlyingType());
} else if (type.getKind().isPrimitive()) {
qname = type.getUnderlyingType().toString();
} else {
qname = null;
}
if (qname != null) {
Set<AnnotationMirror> fromQname = types.get(qname);
if (fromQname != null) {
type.addMissingAnnotations(fromQname);
}
}
// If the type's kind or class is in the appropriate map, annotate the type.
Set<AnnotationMirror> fromKind = typeKinds.get(type.getKind());
if (fromKind != null) {
type.addMissingAnnotations(fromKind);
} else if (!atmClasses.isEmpty()) {
Class<? extends AnnotatedTypeMirror> t = type.getClass();
Set<AnnotationMirror> fromClass = atmClasses.get(t);
if (fromClass != null) {
type.addMissingAnnotations(fromClass);
}
}
return super.scan(type, p);
}
/**
* Adds standard rules. Currently, sets Void to bottom if no other qualifier is set for Void.
* Also, see {@link LiteralTreeAnnotator#addStandardLiteralQualifiers()}.
*
* @return this
*/
public DefaultForTypeAnnotator addStandardDefaults() {
if (!types.containsKey(Void.class.getCanonicalName())) {
for (AnnotationMirror bottom : qualHierarchy.getBottomAnnotations()) {
addTypes(Void.class, bottom);
}
} else {
Set<AnnotationMirror> annos = types.get(Void.class.getCanonicalName());
for (AnnotationMirror top : qualHierarchy.getTopAnnotations()) {
if (qualHierarchy.findAnnotationInHierarchy(annos, top) == null) {
addTypes(Void.class, qualHierarchy.getBottomAnnotation(top));
}
}
}
return this;
}
/**
* Apply defaults based on a variable name to a type.
*
* @param type a type to apply defaults to
* @param name the name of the variable that has type {@code type}, or the name of the method
* whose return type is {@code type}
*/
public void defaultTypeFromName(AnnotatedTypeMirror type, String name) {
// TODO: Check whether the annotation is applicable to this Java type?
AnnotationMirror defaultAnno = listOfNameRegexes.getDefaultAnno(name);
if (defaultAnno != null) {
if (typeFactory
.getQualifierHierarchy()
.findAnnotationInHierarchy(type.getAnnotations(), defaultAnno)
== null) {
type.addAnnotation(defaultAnno);
}
}
}
@Override
public Void visitExecutable(AnnotatedExecutableType type, Void aVoid) {
ExecutableElement element = type.getElement();
Iterator<AnnotatedTypeMirror> paramTypes = type.getParameterTypes().iterator();
for (VariableElement paramElt : element.getParameters()) {
String paramName = paramElt.getSimpleName().toString();
AnnotatedTypeMirror paramType = paramTypes.next();
defaultTypeFromName(paramType, paramName);
}
String methodName = element.getSimpleName().toString();
AnnotatedTypeMirror returnType = type.getReturnType();
defaultTypeFromName(returnType, methodName);
return super.visitExecutable(type, aVoid);
}
/**
* A list where each element associates an annotation with name regexes and name exception
* regexes.
*/
private static class ListOfNameRegexes extends ArrayList<NameRegexes> {
static final long serialVersionUID = 20200218L;
/**
* Update this list from the {@code names} and {@code namesExceptions} fields of a @DefaultFor
* annotation.
*
* @param theQual the qualifier that a @DefaultFor annotation is written on
* @param defaultFor the @DefaultFor annotation written on {@code theQual}
*/
void add(AnnotationMirror theQual, DefaultFor defaultFor) {
if (defaultFor.names().length != 0) {
NameRegexes thisName = new NameRegexes(theQual);
for (String nameRegex : defaultFor.names()) {
try {
thisName.names.add(Pattern.compile(nameRegex));
} catch (PatternSyntaxException e) {
throw new TypeSystemError(
"In annotation %s, names() value \"%s\" is not a regular expression",
theQual, nameRegex);
}
}
for (String namesExceptionsRegex : defaultFor.namesExceptions()) {
try {
thisName.namesExceptions.add(Pattern.compile(namesExceptionsRegex));
} catch (PatternSyntaxException e) {
throw new TypeSystemError(
"In annotation %s, namesExceptions() value \"%s\" is not a regular expression",
theQual, namesExceptionsRegex);
}
}
add(thisName);
} else if (defaultFor.namesExceptions().length != 0) {
throw new TypeSystemError(
"On annotation %s, %s has empty names() but nonempty namesExceptions()",
theQual, defaultFor);
}
}
/**
* Returns the annotation that should be the default for a variable of the given name, or for
* the return type of a method of the given name.
*
* @param name a variable name
* @return the annotation that should be the default for a variable named {@code name}, or null
* if none
*/
@Nullable AnnotationMirror getDefaultAnno(String name) {
if (this.isEmpty()) {
return null;
}
AnnotationMirror result = null;
for (NameRegexes nameRegexes : this) {
if (nameRegexes.matches(name)) {
if (result == null) {
result = nameRegexes.anno;
} else {
// This could combine the annotatations instead, but I think doing so
// silently would confuse users.
throw new TypeSystemError(
"Multiple annotations are applicable to the name \"%s\"", name);
}
}
}
return result;
}
}
/**
* Associates an annotation with the variable names that cause the annotation to be chosen as a
* default.
*/
private static class NameRegexes {
/** The annotation. */
final AnnotationMirror anno;
/** The name regexes. */
final List<Pattern> names = new ArrayList<>(0);
/** The name exception regexes. */
final List<Pattern> namesExceptions = new ArrayList<>(0);
/**
* Constructs a NameRegexes from a @DefaultFor annotation.
*
* @param theQual the qualifier that {@code defaultFor} is written on
*/
NameRegexes(AnnotationMirror theQual) {
this.anno = theQual;
}
/**
* Returns true if the regular expressions match the given name -- that is, if {@link #anno}
* should be used as the default type for a variable named {@code name}, or for the return type
* of a method named {@code name}.
*
* @param name a variable or method name
* @return true if {@link #anno} should be used as the default for a variable named {@code name}
*/
public boolean matches(String name) {
return names.stream().anyMatch(p -> p.matcher(name).matches())
&& namesExceptions.stream().noneMatch(p -> p.matcher(name).matches());
}
}
}