blob: 8396e68ea5695e938a5f37d9fda932364fced890 [file] [log] [blame]
package org.checkerframework.common.initializedfields;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Options;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.common.accumulation.AccumulationAnnotatedTypeFactory;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.common.initializedfields.qual.EnsuresInitializedFields;
import org.checkerframework.common.initializedfields.qual.InitializedFields;
import org.checkerframework.common.initializedfields.qual.InitializedFieldsBottom;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.util.Contract;
import org.checkerframework.framework.util.ContractsFromMethod;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.UserError;
/** The annotated type factory for the Initialized Fields Checker. */
public class InitializedFieldsAnnotatedTypeFactory extends AccumulationAnnotatedTypeFactory {
/**
* The type factories that determine whether the default value is consistent with the annotated
* type. If empty, warn about all uninitialized fields.
*/
List<GenericAnnotatedTypeFactory<?, ?, ?, ?>> defaultValueAtypeFactories;
/**
* Creates a new InitializedFieldsAnnotatedTypeFactory.
*
* @param checker the checker
*/
public InitializedFieldsAnnotatedTypeFactory(BaseTypeChecker checker) {
super(checker, InitializedFields.class, InitializedFieldsBottom.class);
String[] checkerNames = getCheckerNames();
defaultValueAtypeFactories = new ArrayList<>();
for (String checkerName : checkerNames) {
if (checkerName.equals(InitializedFieldsChecker.class.getCanonicalName())) {
continue;
}
@SuppressWarnings("signature:argument") // -processor is a binary name
GenericAnnotatedTypeFactory<?, ?, ?, ?> atf = getTypeFactory(checkerName);
if (atf != null) {
defaultValueAtypeFactories.add(atf);
}
}
this.postInit();
}
/**
* Returns the names of the annotation processors that are being run.
*
* @return the names of the annotation processors that are being run
*/
@SuppressWarnings("JdkObsolete") // ClassLoader.getResources returns an Enumeration
private String[] getCheckerNames() {
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
String processorArg = Options.instance(context).get("-processor");
if (processorArg != null) {
return processorArg.split(",");
}
try {
String filename = "META-INF/services/javax.annotation.processing.Processor";
List<String> lines = new ArrayList<>();
Enumeration<URL> urls = getClass().getClassLoader().getResources(filename);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
lines.addAll(in.lines().collect(Collectors.toList()));
}
String[] result = lines.toArray(new String[0]);
return result;
} catch (IOException e) {
throw new BugInCF(e);
}
}
/**
* Returns the type factory for the given annotation processor, if it is type-checker.
*
* @param processorName the fully-qualified class name of an annotation processor
* @return the type factory for the given annotation processor, or null if it's not a checker
*/
GenericAnnotatedTypeFactory<?, ?, ?, ?> getTypeFactory(@BinaryName String processorName) {
try {
Class<?> checkerClass = Class.forName(processorName);
if (!BaseTypeChecker.class.isAssignableFrom(checkerClass)) {
return null;
}
@SuppressWarnings("unchecked")
BaseTypeChecker c =
((Class<? extends BaseTypeChecker>) checkerClass).getDeclaredConstructor().newInstance();
c.init(processingEnv);
c.initChecker();
BaseTypeVisitor<?> v = c.createSourceVisitorPublic();
GenericAnnotatedTypeFactory<?, ?, ?, ?> atf = v.createTypeFactoryPublic();
if (atf == null) {
throw new UserError("Cannot find %s; check the classpath or processorpath", processorName);
}
return atf;
} catch (ClassNotFoundException
| InstantiationException
| InvocationTargetException
| IllegalAccessException
| NoSuchMethodException e) {
throw new UserError("Problem instantiating " + processorName, e);
}
}
@Override
public InitializedFieldsContractsFromMethod getContractsFromMethod() {
return new InitializedFieldsContractsFromMethod(this);
}
/**
* A subclass of ContractsFromMethod that adds a postcondition contract to each constructor,
* requiring that it initializes all fields.
*/
private class InitializedFieldsContractsFromMethod extends ContractsFromMethod {
/**
* Creates an InitializedFieldsContractsFromMethod for the given factory.
*
* @param factory the type factory associated with the newly-created ContractsFromMethod
*/
public InitializedFieldsContractsFromMethod(GenericAnnotatedTypeFactory<?, ?, ?, ?> factory) {
super(factory);
}
@Override
public Set<Contract.Postcondition> getPostconditions(ExecutableElement executableElement) {
Set<Contract.Postcondition> result = super.getPostconditions(executableElement);
// Only process methods defined in source code being type-checked.
if (declarationFromElement(executableElement) != null) {
if (executableElement.getKind() == ElementKind.CONSTRUCTOR) {
// It's a constructor
String[] fieldsToInitialize =
fieldsToInitialize((TypeElement) executableElement.getEnclosingElement());
if (fieldsToInitialize.length != 0) {
AnnotationMirror initializedFieldsAnno;
{
AnnotationBuilder builder =
new AnnotationBuilder(processingEnv, InitializedFields.class);
builder.setValue("value", fieldsToInitialize);
initializedFieldsAnno = builder.build();
}
AnnotationMirror ensuresAnno;
{
AnnotationBuilder builder =
new AnnotationBuilder(processingEnv, EnsuresInitializedFields.class);
builder.setValue("value", new String[] {"this"});
builder.setValue("fields", fieldsToInitialize);
ensuresAnno = builder.build();
}
Contract.Postcondition ensuresContract =
new Contract.Postcondition("this", initializedFieldsAnno, ensuresAnno);
result.add(ensuresContract);
}
}
}
return result;
}
}
/**
* Returns the fields that the constructor must initialize. These are the fields F declared in
* this class that satisfy all of the following conditions:
*
* <ul>
* <li>F is a non-final field (if final, Java will issue a warning, so we don't need to).
* <li>F's declaration has no initializer.
* <li>No initialization block or static initialization block sets the field. (This is handled
* automatically because dataflow visits (static) initialization blocks as part of the
* constructor.)
* <li>F's annotated type is not consistent with the default value (0, 0.0, false, or null)
* </ul>
*
* @param type the type whose fields to list
* @return the fields whose type is not consistent with the default value, so the constructor must
* initialize them
*/
// It is a bit wasteful that this is recomputed for each constructor.
private String[] fieldsToInitialize(TypeElement type) {
List<String> result = new ArrayList<String>();
for (Element member : type.getEnclosedElements()) {
if (member.getKind() != ElementKind.FIELD) {
continue;
}
VariableElement field = (VariableElement) member;
if (ElementUtils.isFinal(field)) {
continue;
}
VariableTree fieldTree = (VariableTree) declarationFromElement(field);
if (fieldTree.getInitializer() != null) {
continue;
}
if (!defaultValueIsOK(field)) {
result.add(field.getSimpleName().toString());
}
}
return result.toArray(new String[result.size()]);
}
/**
* Returns true if the default field value (0, false, or null) is consistent with the field's
* declared type.
*
* @param field a field
* @return true if the default field value is consistent with the field's declared type
*/
private boolean defaultValueIsOK(VariableElement field) {
if (defaultValueAtypeFactories.isEmpty()) {
return false;
}
for (GenericAnnotatedTypeFactory<?, ?, ?, ?> defaultValueAtypeFactory :
defaultValueAtypeFactories) {
defaultValueAtypeFactory.setRoot(root);
AnnotatedTypeMirror fieldType = defaultValueAtypeFactory.getAnnotatedType(field);
AnnotatedTypeMirror defaultValueType =
defaultValueAtypeFactory.getDefaultValueAnnotatedType(fieldType.getUnderlyingType());
if (!defaultValueAtypeFactory.getTypeHierarchy().isSubtype(defaultValueType, fieldType)) {
return false;
}
}
return true;
}
}