blob: 18e6754e141f871262b8be89e13add53b6e61f5f [file] [log] [blame]
package org.checkerframework.common.wholeprograminference;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.CallableDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.EnumDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.ReceiverParameter;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.visitor.CloneVisitor;
import com.github.javaparser.printer.DefaultPrettyPrinter;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.wholeprograminference.WholeProgramInference.OutputFormat;
import org.checkerframework.dataflow.analysis.Analysis;
import org.checkerframework.framework.ajava.AnnotationMirrorToAnnotationExprConversion;
import org.checkerframework.framework.ajava.AnnotationTransferVisitor;
import org.checkerframework.framework.ajava.DefaultJointVisitor;
import org.checkerframework.framework.ajava.JointJavacJavaParserVisitor;
import org.checkerframework.framework.qual.TypeUseLocation;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.util.JavaParserUtil;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;
import scenelib.annotations.util.JVMNames;
/**
* This is an implementation of {@link WholeProgramInferenceStorage} that stores annotations
* directly with the JavaParser node corresponding to the annotation's location. It outputs ajava
* files.
*/
public class WholeProgramInferenceJavaParserStorage
implements WholeProgramInferenceStorage<AnnotatedTypeMirror> {
/**
* Directory where .ajava files will be written to and read from. This directory is relative to
* where the javac command is executed.
*/
public static final String AJAVA_FILES_PATH =
"build" + File.separator + "whole-program-inference" + File.separator;
/** The type factory associated with this. */
protected final AnnotatedTypeFactory atypeFactory;
/**
* Maps from binary class name to the wrapper containing the class. Contains all classes in Java
* source files containing an Element for which an annotation has been inferred.
*/
private Map<@BinaryName String, ClassOrInterfaceAnnos> classToAnnos = new HashMap<>();
/**
* Files containing classes for which an annotation has been inferred since the last time files
* were written to disk.
*/
private Set<String> modifiedFiles = new HashSet<>();
/** Mapping from source file to the wrapper for the compilation unit parsed from that file. */
private Map<String, CompilationUnitAnnos> sourceToAnnos = new HashMap<>();
/**
* Constructs a new {@code WholeProgramInferenceJavaParser} that has not yet inferred any
* annotations.
*
* @param atypeFactory the associated type factory
*/
public WholeProgramInferenceJavaParserStorage(AnnotatedTypeFactory atypeFactory) {
this.atypeFactory = atypeFactory;
}
@Override
public String getFileForElement(Element elt) {
return addClassesForElement(elt);
}
@Override
public void setFileModified(String path) {
modifiedFiles.add(path);
}
///
/// Reading stored annotations
///
@Override
public boolean hasStorageLocationForMethod(ExecutableElement methodElt) {
return getMethodAnnos(methodElt) != null;
}
/**
* Get the annotations for a method or constructor.
*
* @param methodElt the method or constructor
* @return the annotations for a method or constructor
*/
private CallableDeclarationAnnos getMethodAnnos(ExecutableElement methodElt) {
String className = ElementUtils.getEnclosingClassName(methodElt);
// Read in classes for the element.
getFileForElement(methodElt);
ClassOrInterfaceAnnos classAnnos = classToAnnos.get(className);
CallableDeclarationAnnos methodAnnos =
classAnnos.callableDeclarations.get(JVMNames.getJVMMethodSignature(methodElt));
return methodAnnos;
}
@Override
public AnnotatedTypeMirror getParameterAnnotations(
ExecutableElement methodElt,
int i,
AnnotatedTypeMirror paramATM,
VariableElement ve,
AnnotatedTypeFactory atypeFactory) {
CallableDeclarationAnnos methodAnnos = getMethodAnnos(methodElt);
return methodAnnos.getParameterTypeInitialized(paramATM, i, atypeFactory);
}
@Override
public AnnotatedTypeMirror getReceiverAnnotations(
ExecutableElement methodElt,
AnnotatedTypeMirror paramATM,
AnnotatedTypeFactory atypeFactory) {
CallableDeclarationAnnos methodAnnos = getMethodAnnos(methodElt);
return methodAnnos.getReceiverType(paramATM, atypeFactory);
}
@Override
public AnnotatedTypeMirror getReturnAnnotations(
ExecutableElement methodElt, AnnotatedTypeMirror atm, AnnotatedTypeFactory atypeFactory) {
CallableDeclarationAnnos methodAnnos = getMethodAnnos(methodElt);
return methodAnnos.getReturnType(atm, atypeFactory);
}
@Override
public AnnotatedTypeMirror getFieldAnnotations(
Element element,
String fieldName,
AnnotatedTypeMirror lhsATM,
AnnotatedTypeFactory atypeFactory) {
ClassSymbol enclosingClass = ((VarSymbol) element).enclClass();
// Read in classes for the element.
getFileForElement(element);
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/3094
@BinaryName String className = enclosingClass.flatname.toString();
ClassOrInterfaceAnnos classAnnos = classToAnnos.get(className);
return classAnnos.fields.get(fieldName).getType(lhsATM, atypeFactory);
}
@Override
public AnnotatedTypeMirror getPreOrPostconditionsForField(
Analysis.BeforeOrAfter preOrPost,
ExecutableElement methodElement,
VariableElement fieldElement,
AnnotatedTypeFactory atypeFactory) {
switch (preOrPost) {
case BEFORE:
return getPreconditionsForField(methodElement, fieldElement, atypeFactory);
case AFTER:
return getPostconditionsForField(methodElement, fieldElement, atypeFactory);
default:
throw new BugInCF("Unexpected " + preOrPost);
}
}
/**
* Returns the precondition annotations for a field.
*
* @param methodElement the method
* @param fieldElement the field
* @param atypeFactory the type factory
* @return the precondition annotations for a field
*/
private AnnotatedTypeMirror getPreconditionsForField(
ExecutableElement methodElement,
VariableElement fieldElement,
AnnotatedTypeFactory atypeFactory) {
CallableDeclarationAnnos methodAnnos = getMethodAnnos(methodElement);
return methodAnnos.getPreconditionsForField(fieldElement, atypeFactory);
}
/**
* Returns the postcondition annotations for a field.
*
* @param methodElement the method
* @param fieldElement the field
* @param atypeFactory the type factory
* @return the postcondition annotations for a field
*/
private AnnotatedTypeMirror getPostconditionsForField(
ExecutableElement methodElement,
VariableElement fieldElement,
AnnotatedTypeFactory atypeFactory) {
CallableDeclarationAnnos methodAnnos = getMethodAnnos(methodElement);
return methodAnnos.getPostconditionsForField(fieldElement, atypeFactory);
}
@Override
public boolean addMethodDeclarationAnnotation(
ExecutableElement methodElt, AnnotationMirror anno) {
CallableDeclarationAnnos methodAnnos = getMethodAnnos(methodElt);
boolean isNewAnnotation = methodAnnos.addDeclarationAnnotation(anno);
if (isNewAnnotation) {
modifiedFiles.add(getFileForElement(methodElt));
}
return isNewAnnotation;
}
@Override
public AnnotatedTypeMirror atmFromStorageLocation(
TypeMirror typeMirror, AnnotatedTypeMirror storageLocation) {
return storageLocation;
}
@Override
public void updateStorageLocationFromAtm(
AnnotatedTypeMirror newATM,
AnnotatedTypeMirror curATM,
AnnotatedTypeMirror typeToUpdate,
TypeUseLocation defLoc,
boolean ignoreIfAnnotated) {
// Clears only the annotations that are supported by atypeFactory.
// The others stay intact.
Set<AnnotationMirror> annosToRemove = AnnotationUtils.createAnnotationSet();
for (AnnotationMirror anno : typeToUpdate.getAnnotations()) {
if (atypeFactory.isSupportedQualifier(anno)) {
annosToRemove.add(anno);
}
}
// This method may be called consecutive times to modify the same AnnotatedTypeMirror.
// Each time it is called, the AnnotatedTypeMirror has a better type
// estimate for the modified AnnotatedTypeMirror. Therefore, it is not a problem to remove
// all annotations before inserting the new annotations.
typeToUpdate.removeAnnotations(annosToRemove);
// Only update the AnnotatedTypeMirror if there are no explicit annotations
if (curATM.getExplicitAnnotations().isEmpty() || !ignoreIfAnnotated) {
for (AnnotationMirror am : newATM.getAnnotations()) {
typeToUpdate.addAnnotation(am);
}
} else if (curATM.getKind() == TypeKind.TYPEVAR) {
// getExplicitAnnotations will be non-empty for type vars whose bounds are explicitly
// annotated. So instead, only insert the annotation if there is not primary annotation
// of the same hierarchy.
for (AnnotationMirror am : newATM.getAnnotations()) {
if (curATM.getAnnotationInHierarchy(am) != null) {
// Don't insert if the type is already has a primary annotation
// in the same hierarchy.
break;
}
typeToUpdate.addAnnotation(am);
}
}
if (newATM.getKind() == TypeKind.ARRAY) {
AnnotatedArrayType newAAT = (AnnotatedArrayType) newATM;
AnnotatedArrayType oldAAT = (AnnotatedArrayType) curATM;
AnnotatedArrayType aatToUpdate = (AnnotatedArrayType) typeToUpdate;
updateStorageLocationFromAtm(
newAAT.getComponentType(),
oldAAT.getComponentType(),
aatToUpdate.getComponentType(),
defLoc,
ignoreIfAnnotated);
}
}
///
/// Reading in files
///
@Override
public void preprocessClassTree(ClassTree classTree) {
addClassTree(classTree);
}
/**
* Reads in the source file containing {@code tree} and creates wrappers around all classes in the
* file. Stores the wrapper for the compilation unit in {@link #sourceToAnnos} and stores the
* wrappers of all classes in the file in {@link #classToAnnos}.
*
* @param tree tree for class to add
*/
private void addClassTree(ClassTree tree) {
TypeElement element = TreeUtils.elementFromDeclaration(tree);
String className = ElementUtils.getBinaryName(element);
if (classToAnnos.containsKey(className)) {
return;
}
TypeElement toplevelClass = ElementUtils.toplevelEnclosingTypeElement(element);
String path = ElementUtils.getSourceFilePath(toplevelClass);
addSourceFile(path);
CompilationUnitAnnos sourceAnnos = sourceToAnnos.get(path);
TypeDeclaration<?> javaParserNode =
sourceAnnos.getClassOrInterfaceDeclarationByName(toplevelClass.getSimpleName().toString());
ClassTree toplevelClassTree = atypeFactory.getTreeUtils().getTree(toplevelClass);
createWrappersForClass(toplevelClassTree, javaParserNode, sourceAnnos);
}
/**
* Reads in the file at {@code path} and creates a wrapper around its compilation unit. Stores the
* wrapper in {@link #sourceToAnnos}, but doesn't create wrappers around any classes in the file.
*
* @param path path to source file to read
*/
private void addSourceFile(String path) {
if (sourceToAnnos.containsKey(path)) {
return;
}
CompilationUnit root;
try {
root = JavaParserUtil.parseCompilationUnit(new File(path));
} catch (FileNotFoundException e) {
throw new BugInCF("Failed to read Java file " + path, e);
}
JavaParserUtil.concatenateAddedStringLiterals(root);
CompilationUnitAnnos sourceAnnos = new CompilationUnitAnnos(root);
sourceToAnnos.put(path, sourceAnnos);
}
/**
* The first two arugments are a javac tree and a JavaParser node representing the same class.
* This method creates wrappers around all the classes, fields, and methods in that class, and
* stores those wrappers in {@code sourceAnnos}.
*
* @param javacClass javac tree for class
* @param javaParserClass JavaParser node corresponding to the same class as {@code javacClass}
* @param sourceAnnos compilation unit wrapper to add new wrappers to
*/
private void createWrappersForClass(
ClassTree javacClass, TypeDeclaration<?> javaParserClass, CompilationUnitAnnos sourceAnnos) {
JointJavacJavaParserVisitor visitor =
new DefaultJointVisitor() {
@Override
public void processClass(
ClassTree javacTree, ClassOrInterfaceDeclaration javaParserNode) {
addClass(javacTree);
}
@Override
public void processClass(ClassTree javacTree, EnumDeclaration javaParserNode) {
addClass(javacTree);
}
@Override
public void processNewClass(NewClassTree javacTree, ObjectCreationExpr javaParserNode) {
if (javacTree.getClassBody() != null) {
addClass(javacTree.getClassBody());
}
}
/**
* Creates a wrapper around the class for {@code tree} and stores it in {@code
* sourceAnnos}.
*
* @param tree tree to add
*/
private void addClass(ClassTree tree) {
TypeElement classElt = TreeUtils.elementFromDeclaration(tree);
String className = ElementUtils.getBinaryName(classElt);
ClassOrInterfaceAnnos typeWrapper = new ClassOrInterfaceAnnos();
if (!classToAnnos.containsKey(className)) {
classToAnnos.put(className, typeWrapper);
}
sourceAnnos.types.add(typeWrapper);
}
@Override
public void processMethod(MethodTree javacTree, MethodDeclaration javaParserNode) {
addCallableDeclaration(javacTree, javaParserNode);
}
@Override
public void processMethod(MethodTree javacTree, ConstructorDeclaration javaParserNode) {
addCallableDeclaration(javacTree, javaParserNode);
}
/**
* Creates a wrapper around {@code javacTree} with the corresponding declaration {@code
* javaParserNode} and stores it in {@code sourceAnnos}.
*
* @param javacTree javac tree for declaration to add
* @param javaParserNode JavaParser node for the same class as {@code javacTree}
*/
private void addCallableDeclaration(
MethodTree javacTree, CallableDeclaration<?> javaParserNode) {
ExecutableElement elt = TreeUtils.elementFromDeclaration(javacTree);
String className = ElementUtils.getEnclosingClassName(elt);
ClassOrInterfaceAnnos enclosingClass = classToAnnos.get(className);
String executableSignature = JVMNames.getJVMMethodSignature(javacTree);
if (!enclosingClass.callableDeclarations.containsKey(executableSignature)) {
enclosingClass.callableDeclarations.put(
executableSignature, new CallableDeclarationAnnos(javaParserNode));
}
}
@Override
public void processVariable(VariableTree javacTree, VariableDeclarator javaParserNode) {
// This seems to occur when javacTree is a local variable in the second
// class located in a source file. If this check returns false, then the
// below call to TreeUtils.elementFromDeclaration causes a crash.
if (TreeUtils.elementFromTree(javacTree) == null) {
return;
}
VariableElement elt = TreeUtils.elementFromDeclaration(javacTree);
if (!elt.getKind().isField()) {
return;
}
String enclosingClassName = ElementUtils.getEnclosingClassName(elt);
ClassOrInterfaceAnnos enclosingClass = classToAnnos.get(enclosingClassName);
String fieldName = javacTree.getName().toString();
if (!enclosingClass.fields.containsKey(fieldName)) {
enclosingClass.fields.put(fieldName, new FieldAnnos(javaParserNode));
}
}
};
visitor.visitClass(javacClass, javaParserClass);
}
/**
* Calls {@link #addSourceFile(String)} for the file containing the given element.
*
* @param element the element for the source file to add
* @return path of the file containing {@code element}
*/
private String addClassesForElement(Element element) {
if (!ElementUtils.isElementFromSourceCode(element)) {
throw new BugInCF("Called addClassesForElement for non-source element: " + element);
}
TypeElement toplevelClass = ElementUtils.toplevelEnclosingTypeElement(element);
String path = ElementUtils.getSourceFilePath(toplevelClass);
if (classToAnnos.containsKey(ElementUtils.getBinaryName(toplevelClass))) {
return path;
}
addSourceFile(path);
CompilationUnitAnnos sourceAnnos = sourceToAnnos.get(path);
ClassTree toplevelClassTree = (ClassTree) atypeFactory.declarationFromElement(toplevelClass);
TypeDeclaration<?> javaParserNode =
sourceAnnos.getClassOrInterfaceDeclarationByName(toplevelClass.getSimpleName().toString());
createWrappersForClass(toplevelClassTree, javaParserNode, sourceAnnos);
return path;
}
///
/// Writing to a file
///
// The prepare*ForWriting hooks are needed in addition to the postProcessClassTree hook because
// a scene may be modifed and written at any time, including before or after
// postProcessClassTree is called.
/**
* Side-effects the compilation unit annotations to make any desired changes before writing to a
* file.
*
* @param compilationUnitAnnos the compilation unit annotations to modify
*/
public void prepareCompilationUnitForWriting(CompilationUnitAnnos compilationUnitAnnos) {
for (ClassOrInterfaceAnnos type : compilationUnitAnnos.types) {
prepareClassForWriting(type);
}
}
/**
* Side-effects the class annotations to make any desired changes before writing to a file.
*
* @param classAnnos the class annotations to modify
*/
public void prepareClassForWriting(ClassOrInterfaceAnnos classAnnos) {
for (Map.Entry<String, CallableDeclarationAnnos> methodEntry :
classAnnos.callableDeclarations.entrySet()) {
prepareMethodForWriting(methodEntry.getValue());
}
}
/**
* Side-effects the method or constructor annotations to make any desired changes before writing
* to a file.
*
* @param methodAnnos the method or constructor annotations to modify
*/
public void prepareMethodForWriting(CallableDeclarationAnnos methodAnnos) {
atypeFactory.prepareMethodForWriting(methodAnnos);
}
@Override
public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checker) {
if (outputFormat != OutputFormat.AJAVA) {
throw new BugInCF("WholeProgramInferenceJavaParser used with format " + outputFormat);
}
File outputDir = new File(AJAVA_FILES_PATH);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
for (String path : modifiedFiles) {
CompilationUnitAnnos root = sourceToAnnos.get(path);
prepareCompilationUnitForWriting(root);
root.transferAnnotations();
String packageDir = AJAVA_FILES_PATH;
if (root.compilationUnit.getPackageDeclaration().isPresent()) {
packageDir +=
File.separator
+ root.compilationUnit
.getPackageDeclaration()
.get()
.getNameAsString()
.replaceAll("\\.", File.separator);
}
File packageDirFile = new File(packageDir);
if (!packageDirFile.exists()) {
packageDirFile.mkdirs();
}
String name = new File(path).getName();
if (name.endsWith(".java")) {
name = name.substring(0, name.length() - ".java".length());
}
name += "-" + checker.getClass().getCanonicalName() + ".ajava";
String outputPath = packageDir + File.separator + name;
try {
FileWriter writer = new FileWriter(outputPath);
// JavaParser can output using lexical preserving printing, which writes the file such that
// its formatting is close to the original source file it was parsed from as
// possible. Currently, this feature is very buggy and crashes when adding annotations in
// certain locations. This implementation could be used instead if it's fixed in JavaParser.
// LexicalPreservingPrinter.print(root.declaration, writer);
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
writer.write(prettyPrinter.print(root.compilationUnit));
writer.close();
} catch (IOException e) {
throw new BugInCF("Error while writing ajava file " + outputPath, e);
}
}
modifiedFiles.clear();
}
/**
* Adds an explicit receiver type to a JavaParser method declaration.
*
* @param methodDeclaration declaration to add a receiver to
*/
private static void addExplicitReceiver(MethodDeclaration methodDeclaration) {
if (methodDeclaration.getReceiverParameter().isPresent()) {
return;
}
com.github.javaparser.ast.Node parent = methodDeclaration.getParentNode().get();
if (!(parent instanceof TypeDeclaration)) {
return;
}
TypeDeclaration<?> parentDecl = (TypeDeclaration<?>) parent;
ClassOrInterfaceType receiver = new ClassOrInterfaceType();
receiver.setName(parentDecl.getName());
if (parentDecl.isClassOrInterfaceDeclaration()) {
ClassOrInterfaceDeclaration parentClassDecl = parentDecl.asClassOrInterfaceDeclaration();
if (!parentClassDecl.getTypeParameters().isEmpty()) {
NodeList<Type> typeArgs = new NodeList<>();
for (TypeParameter typeParam : parentClassDecl.getTypeParameters()) {
ClassOrInterfaceType typeArg = new ClassOrInterfaceType();
typeArg.setName(typeParam.getNameAsString());
typeArgs.add(typeArg);
}
receiver.setTypeArguments(typeArgs);
}
}
methodDeclaration.setReceiverParameter(new ReceiverParameter(receiver, "this"));
}
/**
* Transfers all annotations for {@code annotatedType} and its nested types to {@code target},
* which is the JavaParser node representing the same type. Does nothing if {@code annotatedType}
* is null (this may occur if there's no inferred annotations for the type).
*
* @param annotatedType type to transfer annotations from
* @param target the JavaParser type to transfer annotation to; must represent the same type as
* {@code annotatedType}
*/
private static void transferAnnotations(
@Nullable AnnotatedTypeMirror annotatedType, Type target) {
if (annotatedType == null) {
return;
}
target.accept(new AnnotationTransferVisitor(), annotatedType);
}
///
/// Storing annotations
///
/**
* Stores the JavaParser node for a compilation unit and the list of wrappers for the classes and
* interfaces in that compilation unit.
*/
private static class CompilationUnitAnnos {
/** Compilation unit being wrapped. */
public CompilationUnit compilationUnit;
/** Wrappers for classes and interfaces in {@code declaration} */
public List<ClassOrInterfaceAnnos> types;
/**
* Constructs a wrapper around the given compilation unit.
*
* @param compilationUnit compilation unit to wrap
*/
public CompilationUnitAnnos(CompilationUnit compilationUnit) {
this.compilationUnit = compilationUnit;
types = new ArrayList<>();
}
/**
* Transfers all annotations inferred by whole program inference for the wrapped compilation
* unit to their corresponding JavaParser locations.
*/
public void transferAnnotations() {
JavaParserUtil.clearAnnotations(compilationUnit);
for (ClassOrInterfaceAnnos typeAnnos : types) {
typeAnnos.transferAnnotations();
}
}
/**
* Returns the top-level type declaration named {@code name} in the compilation unit.
*
* @param name name of type declaration
* @return the type declaration named {@code name} in the wrapped compilation unit
*/
public TypeDeclaration<?> getClassOrInterfaceDeclarationByName(String name) {
return JavaParserUtil.getTypeDeclarationByName(compilationUnit, name);
}
}
/**
* Stores wrappers for the locations where annotations may be inferred in a class or interface.
*/
private static class ClassOrInterfaceAnnos {
/**
* Mapping from JVM method signatures to the wrapper containing the corresponding executable.
*/
public Map<String, CallableDeclarationAnnos> callableDeclarations = new HashMap<>();
/** Mapping from field names to wrappers for those fields. */
public Map<String, FieldAnnos> fields = new HashMap<>(2);
/**
* Transfers all annotations inferred by whole program inference for the methods and fields in
* the wrapper class or interface to their corresponding JavaParser locations.
*/
public void transferAnnotations() {
for (CallableDeclarationAnnos callableAnnos : callableDeclarations.values()) {
callableAnnos.transferAnnotations();
}
for (FieldAnnos field : fields.values()) {
field.transferAnnotations();
}
}
@Override
public String toString() {
return "ClassOrInterfaceAnnos [callableDeclarations="
+ callableDeclarations
+ ", fields="
+ fields
+ "]";
}
}
/**
* Stores the JavaParser node for a method or constructor and the annotations that have been
* inferred about its parameters and return type.
*/
public class CallableDeclarationAnnos {
/** Wrapped method or constructor declaration. */
public CallableDeclaration<?> declaration;
/** Path to file containing the declaration. */
public String file;
/**
* Inferred annotations for the return type, if the declaration represents a method. Initialized
* on first usage.
*/
private @MonotonicNonNull AnnotatedTypeMirror returnType = null;
/**
* Inferred annotations for the receiver type, if the declaration represents a method.
* Initialized on first usage.
*/
private @MonotonicNonNull AnnotatedTypeMirror receiverType = null;
/**
* Inferred annotations for parameter types. Initialized the first time any parameter is
* accessed and each parameter is initialized the first time it's accessed.
*/
private @MonotonicNonNull List<@Nullable AnnotatedTypeMirror> parameterTypes = null;
/** Annotations on the callable declaration. */
private @MonotonicNonNull Set<AnnotationMirror> declarationAnnotations = null;
/**
* Mapping from VariableElements for fields to an AnnotatedTypeMirror containing the inferred
* preconditions on that field.
*/
private @MonotonicNonNull Map<VariableElement, AnnotatedTypeMirror> fieldToPreconditions = null;
/**
* Mapping from VariableElements for fields to an AnnotatedTypeMirror containing the inferred
* postconditions on that field.
*/
private @MonotonicNonNull Map<VariableElement, AnnotatedTypeMirror> fieldToPostconditions =
null;
// /** Inferred contracts for the callable declaration. */
// private @MonotonicNonNull List<AnnotationMirror> contracts = null;
/**
* Creates a wrapper for the given method or constructor declaration.
*
* @param declaration method or constructor declaration to wrap
*/
public CallableDeclarationAnnos(CallableDeclaration<?> declaration) {
this.declaration = declaration;
}
/**
* Returns the inferred type for the parameter at the given index. If necessary, initializes the
* {@code AnnotatedTypeMirror} for that location using {@code type} and {@code atf} to a wrapper
* around the base type for the parameter.
*
* @param type type for the parameter at {@code index}, used for initializing the returned
* {@code AnnotatedTypeMirror} the first time it's accessed
* @param atf the annotated type factory of a given type system, whose type hierarchy will be
* used
* @param index index of the parameter to return the inferred annotations of
* @return an {@code AnnotatedTypeMirror} containing all annotations inferred for the parameter
* at the given index
*/
public AnnotatedTypeMirror getParameterTypeInitialized(
AnnotatedTypeMirror type, int index, AnnotatedTypeFactory atf) {
if (parameterTypes == null) {
parameterTypes =
new ArrayList<>(Collections.nCopies(declaration.getParameters().size(), null));
}
if (parameterTypes.get(index) == null) {
parameterTypes.set(
index, AnnotatedTypeMirror.createType(type.getUnderlyingType(), atf, false));
}
return parameterTypes.get(index);
}
/**
* Returns the inferred type for the parameter at the given index, or null if there's no
* parameter at the given index or there's no inferred type for that parameter.
*
* @param index index of the parameter to return the inferred annotations of
* @return an {@code AnnotatedTypeMirror} containing all annotations inferred for the parameter
* at the given index, or null if there's no parameter at {@code index} or if there's not
* inferred annotations for that parameter
*/
public @Nullable AnnotatedTypeMirror getParameterType(int index) {
if (parameterTypes == null || index < 0 || index >= parameterTypes.size()) {
return null;
}
return parameterTypes.get(index);
}
/**
* Returns the inferred declaration annotations on this executable, or null if there are no
* annotations.
*
* @return the declaration annotations for this callable declaration
*/
public Set<AnnotationMirror> getDeclarationAnnotations() {
if (declarationAnnotations == null) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(declarationAnnotations);
}
/**
* Adds a declaration annotation to this callable declaration and returns whether it was a new
* annotation.
*
* @param annotation declaration annotation to add
* @return true if {@code annotation} wasn't previously stored for this callable declaration
*/
public boolean addDeclarationAnnotation(AnnotationMirror annotation) {
if (declarationAnnotations == null) {
declarationAnnotations = new HashSet<>();
}
return declarationAnnotations.add(annotation);
}
/**
* If this wrapper holds a method, returns the inferred type of the receiver. If necessary,
* initializes the {@code AnnotatedTypeMirror} for that location using {@code type} and {@code
* atf} to a wrapper around the base type for the receiver type.
*
* @param type base type for the receiver type, used for initializing the returned {@code
* AnnotatedTypeMirror} the first time it's accessed
* @param atf the annotated type factory of a given type system, whose type hierarchy will be
* used
* @return an {@code AnnotatedTypeMirror} containing all annotations inferred for the receiver
* type
*/
public AnnotatedTypeMirror getReceiverType(AnnotatedTypeMirror type, AnnotatedTypeFactory atf) {
if (receiverType == null) {
receiverType = AnnotatedTypeMirror.createType(type.getUnderlyingType(), atf, false);
}
return receiverType;
}
/**
* If this wrapper holds a method, returns the inferred type of the return type. If necessary,
* initializes the {@code AnnotatedTypeMirror} for that location using {@code type} and {@code
* atf} to a wrapper around the base type for the return type.
*
* @param type base type for the return type, used for initializing the returned {@code
* AnnotatedTypeMirror} the first time it's accessed
* @param atf the annotated type factory of a given type system, whose type hierarchy will be
* used
* @return an {@code AnnotatedTypeMirror} containing all annotations inferred for the return
* type
*/
public AnnotatedTypeMirror getReturnType(AnnotatedTypeMirror type, AnnotatedTypeFactory atf) {
if (returnType == null) {
returnType = AnnotatedTypeMirror.createType(type.getUnderlyingType(), atf, false);
}
return returnType;
}
/**
* Returns the inferred preconditions for this callable declaration.
*
* @return a mapping from VariableElements for fields to AnnotatedTypeMirrors containing the
* inferred preconditions for those fields.
*/
public Map<VariableElement, AnnotatedTypeMirror> getFieldToPreconditions() {
if (fieldToPreconditions == null) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(fieldToPreconditions);
}
/**
* Returns the inferred postconditions for this callable declaration.
*
* @return a mapping from VariableElements for fields to AnnotatedTypeMirrors containing the
* inferred postconditions for those fields.
*/
public Map<VariableElement, AnnotatedTypeMirror> getFieldToPostconditions() {
if (fieldToPostconditions == null) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(fieldToPostconditions);
}
/**
* Returns an AnnotatedTypeMirror containing the preconditions for the given field.
*
* @param field VariableElement for a field in the enclosing class for this method
* @param atf the annotated type factory of a given type system, whose type hierarchy will be
* used
* @return an {@code AnnotatedTypeMirror} containing the annotations for the inferred
* preconditions for the given field
*/
public AnnotatedTypeMirror getPreconditionsForField(
VariableElement field, AnnotatedTypeFactory atf) {
if (fieldToPreconditions == null) {
fieldToPreconditions = new HashMap<>(1);
}
if (!fieldToPreconditions.containsKey(field)) {
TypeMirror underlyingType = atf.getAnnotatedType(field).getUnderlyingType();
AnnotatedTypeMirror preconditionsType =
AnnotatedTypeMirror.createType(underlyingType, atf, false);
fieldToPreconditions.put(field, preconditionsType);
}
return fieldToPreconditions.get(field);
}
/**
* Returns an AnnotatedTypeMirror containing the postconditions for the given field.
*
* @param field VariableElement for a field in the enclosing class for this method
* @param atf the annotated type factory of a given type system, whose type hierarchy will be
* used
* @return an {@code AnnotatedTypeMirror} containing the annotations for the inferred
* postconditions for the given field
*/
public AnnotatedTypeMirror getPostconditionsForField(
VariableElement field, AnnotatedTypeFactory atf) {
if (fieldToPostconditions == null) {
fieldToPostconditions = new HashMap<>(1);
}
if (!fieldToPostconditions.containsKey(field)) {
TypeMirror underlyingType = atf.getAnnotatedType(field).getUnderlyingType();
AnnotatedTypeMirror postconditionsType =
AnnotatedTypeMirror.createType(underlyingType, atf, false);
fieldToPostconditions.put(field, postconditionsType);
}
return fieldToPostconditions.get(field);
}
/**
* Transfers all annotations inferred by whole program inference for the return type, receiver
* type, and parameter types for the wrapped declaration to their corresponding JavaParser
* locations.
*/
public void transferAnnotations() {
if (atypeFactory instanceof GenericAnnotatedTypeFactory<?, ?, ?, ?>) {
GenericAnnotatedTypeFactory<?, ?, ?, ?> genericAtf =
(GenericAnnotatedTypeFactory<?, ?, ?, ?>) atypeFactory;
for (AnnotationMirror contractAnno : genericAtf.getContractAnnotations(this)) {
declaration.addAnnotation(
AnnotationMirrorToAnnotationExprConversion.annotationMirrorToAnnotationExpr(
contractAnno));
}
}
if (declarationAnnotations != null) {
for (AnnotationMirror annotation : declarationAnnotations) {
declaration.addAnnotation(
AnnotationMirrorToAnnotationExprConversion.annotationMirrorToAnnotationExpr(
annotation));
}
}
if (returnType != null) {
// If a return type exists, then the declaration must be a method, not a constructor.
WholeProgramInferenceJavaParserStorage.transferAnnotations(
returnType, declaration.asMethodDeclaration().getType());
}
if (receiverType != null) {
addExplicitReceiver(declaration.asMethodDeclaration());
// The receiver won't be present for an anonymous class.
if (declaration.getReceiverParameter().isPresent()) {
WholeProgramInferenceJavaParserStorage.transferAnnotations(
receiverType, declaration.getReceiverParameter().get().getType());
}
}
if (parameterTypes == null) {
return;
}
for (int i = 0; i < parameterTypes.size(); i++) {
WholeProgramInferenceJavaParserStorage.transferAnnotations(
parameterTypes.get(i), declaration.getParameter(i).getType());
}
}
@Override
public String toString() {
return "CallableDeclarationAnnos [declaration="
+ declaration
+ ", file="
+ file
+ ", parameterTypes="
+ parameterTypes
+ ", receiverType="
+ receiverType
+ ", returnType="
+ returnType
+ "]";
}
}
/** Stores the JavaParser node for a field and the annotations that have been inferred for it. */
private static class FieldAnnos {
/** Wrapped field declaration. */
public VariableDeclarator declaration;
/** Inferred type for field, initialized the first time it's accessed. */
private @MonotonicNonNull AnnotatedTypeMirror type = null;
/**
* Creates a wrapper for the given field declaration.
*
* @param declaration field declaration to wrap
*/
public FieldAnnos(VariableDeclarator declaration) {
this.declaration = declaration;
}
/**
* Returns the inferred type of the field. If necessary, initializes the {@code
* AnnotatedTypeMirror} for that location using {@code type} and {@code atf} to a wrapper around
* the base type for the field.
*
* @param type base type for the field, used for initializing the returned {@code
* AnnotatedTypeMirror} the first time it's accessed
* @param atf the annotated type factory of a given type system, whose type hierarchy will be
* used
* @return an {@code AnnotatedTypeMirror} containing all annotations inferred for the field
*/
public AnnotatedTypeMirror getType(AnnotatedTypeMirror type, AnnotatedTypeFactory atf) {
if (this.type == null) {
this.type = AnnotatedTypeMirror.createType(type.getUnderlyingType(), atf, false);
}
return this.type;
}
/**
* Transfers all annotations inferred by whole program inference on this field to the JavaParser
* nodes for that field.
*/
public void transferAnnotations() {
if (type == null) {
return;
}
Type newType = (Type) declaration.getType().accept(new CloneVisitor(), null);
WholeProgramInferenceJavaParserStorage.transferAnnotations(type, newType);
declaration.setType(newType);
}
@Override
public String toString() {
return "FieldAnnos [declaration=" + declaration + ", type=" + type + "]";
}
}
}