blob: 7ec2e2294dea31deabfa0259dfb7d9bcd067790b [file] [log] [blame]
package org.checkerframework.common.wholeprograminference;
import com.sun.source.tree.ClassTree;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
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.common.wholeprograminference.scenelib.ASceneWrapper;
import org.checkerframework.dataflow.analysis.Analysis;
import org.checkerframework.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.framework.qual.DefaultFor;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.checkerframework.framework.qual.DefaultQualifierInHierarchy;
import org.checkerframework.framework.qual.InvisibleQualifier;
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.AnnotatedTypeMirror.AnnotatedNullType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Pair;
import org.checkerframework.javacutil.TypeAnnotationUtils;
import org.checkerframework.javacutil.UserError;
import scenelib.annotations.Annotation;
import scenelib.annotations.el.AClass;
import scenelib.annotations.el.AField;
import scenelib.annotations.el.AMethod;
import scenelib.annotations.el.AScene;
import scenelib.annotations.el.ATypeElement;
import scenelib.annotations.el.TypePathEntry;
import scenelib.annotations.io.IndexFileParser;
import scenelib.annotations.util.JVMNames;
/**
* This class stores annotations using scenelib objects.
*
* <p>The set of annotations inferred for a certain class is stored in an {@link
* scenelib.annotations.el.AScene}, which {@code writeScenes()} can write into a file. For example,
* a class {@code my.pakkage.MyClass} will have its members' inferred types stored in a Scene, and
* later written into a file named {@code my.pakkage.MyClass.jaif} if using {@link
* OutputFormat#JAIF}, or {@code my.pakkage.MyClass.astub} if using {@link OutputFormat#STUB}.
*
* <p>This class populates the initial Scenes by reading existing .jaif files on the {@link
* #JAIF_FILES_PATH} directory (regardless of output format). Having more information in those
* initial .jaif files means that the precision achieved by the whole-program inference analysis
* will be better. {@link #writeScenes} rewrites the initial .jaif files and may create new ones.
*/
public class WholeProgramInferenceScenesStorage
implements WholeProgramInferenceStorage<ATypeElement> {
/**
* Directory where .jaif files will be written to and read from. This directory is relative to
* where the CF's javac command is executed.
*/
public static final String JAIF_FILES_PATH =
"build" + File.separator + "whole-program-inference" + File.separator;
/** The type factory associated with this WholeProgramInferenceScenesStorage. */
protected final AnnotatedTypeFactory atypeFactory;
/** Annotations that should not be output to a .jaif or stub file. */
private final AnnotationsInContexts annosToIgnore = new AnnotationsInContexts();
/**
* If true, assignments where the rhs is null are be ignored.
*
* <p>If all assignments to a variable are null (because inference is being done with respect to a
* limited set of uses) then the variable is inferred to have bottom type. That inference is
* unlikely to be correct. To avoid that inference, set this variable to true. When the variable
* is true, if all assignments are null, then none are recorded, no inference is done, and the
* variable remains at its default type.
*/
private final boolean ignoreNullAssignments;
/** Maps .jaif file paths (Strings) to Scenes. Relative to JAIF_FILES_PATH. */
public final Map<String, ASceneWrapper> scenes = new HashMap<>();
/**
* Scenes that were modified since the last time all Scenes were written into .jaif files. Each
* String element of this set is a path (relative to JAIF_FILES_PATH) to the .jaif file of the
* corresponding Scene in the set. It is obtained by passing a class name as argument to the
* {@link #getJaifPath} method.
*
* <p>Modifying a Scene means adding (or changing) a type annotation for a field, method return
* type, or method parameter type in the Scene. (Scenes are modified by the method {@link
* #updateAnnotationSetInScene}.)
*/
public final Set<String> modifiedScenes = new HashSet<>();
/**
* Default constructor.
*
* @param atypeFactory the type factory associated with this WholeProgramInferenceScenesStorage
*/
public WholeProgramInferenceScenesStorage(AnnotatedTypeFactory atypeFactory) {
this.atypeFactory = atypeFactory;
boolean isNullness =
atypeFactory.getClass().getSimpleName().equals("NullnessAnnotatedTypeFactory");
this.ignoreNullAssignments = !isNullness;
}
@Override
public String getFileForElement(Element elt) {
String className;
switch (elt.getKind()) {
case CONSTRUCTOR:
case METHOD:
className = ElementUtils.getEnclosingClassName((ExecutableElement) elt);
break;
case LOCAL_VARIABLE:
className = getEnclosingClassName((LocalVariableNode) elt);
break;
case FIELD:
ClassSymbol enclosingClass = ((VarSymbol) elt).enclClass();
className = enclosingClass.flatname.toString();
break;
default:
throw new BugInCF("What element? %s %s", elt.getKind(), elt);
}
String file = getJaifPath(className);
return file;
}
/**
* Get the annotations for a class.
*
* @param className the name of the class, in binary form
* @param file the path to the file that represents the class
* @param classSymbol optionally, the ClassSymbol representing the class
* @return the annotations for the class
*/
private AClass getClassAnnos(
@BinaryName String className, String file, @Nullable ClassSymbol classSymbol) {
// Possibly reads .jaif file to obtain a Scene.
ASceneWrapper scene = getScene(file);
AClass aClass = scene.getAScene().classes.getVivify(className);
scene.updateSymbolInformation(aClass, classSymbol);
return aClass;
}
/**
* Get the annotations for a method or constructor.
*
* @param methodElt the method or constructor
* @return the annotations for a method or constructor
*/
private AMethod getMethodAnnos(ExecutableElement methodElt) {
String className = ElementUtils.getEnclosingClassName(methodElt);
String file = getFileForElement(methodElt);
AClass classAnnos = getClassAnnos(className, file, ((MethodSymbol) methodElt).enclClass());
AMethod methodAnnos = classAnnos.methods.getVivify(JVMNames.getJVMMethodSignature(methodElt));
methodAnnos.setFieldsFromMethodElement(methodElt);
return methodAnnos;
}
@Override
public boolean hasStorageLocationForMethod(ExecutableElement methodElt) {
// The scenes implementation can always add annotations to a method.
return true;
}
@Override
public ATypeElement getParameterAnnotations(
ExecutableElement methodElt,
int i,
AnnotatedTypeMirror paramATM,
VariableElement ve,
AnnotatedTypeFactory atypeFactory) {
AMethod methodAnnos = getMethodAnnos(methodElt);
AField param =
methodAnnos.vivifyAndAddTypeMirrorToParameter(
i, paramATM.getUnderlyingType(), ve.getSimpleName());
return param.type;
}
@Override
public ATypeElement getReceiverAnnotations(
ExecutableElement methodElt,
AnnotatedTypeMirror paramATM,
AnnotatedTypeFactory atypeFactory) {
AMethod methodAnnos = getMethodAnnos(methodElt);
return methodAnnos.receiver.type;
}
@Override
public ATypeElement getReturnAnnotations(
ExecutableElement methodElt, AnnotatedTypeMirror atm, AnnotatedTypeFactory atypeFactory) {
AMethod methodAnnos = getMethodAnnos(methodElt);
return methodAnnos.returnType;
}
@Override
public ATypeElement getFieldAnnotations(
Element element,
String fieldName,
AnnotatedTypeMirror lhsATM,
AnnotatedTypeFactory atypeFactory) {
ClassSymbol enclosingClass = ((VarSymbol) element).enclClass();
String file = getFileForElement(element);
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/3094
@BinaryName String className = enclosingClass.flatname.toString();
AClass classAnnos = getClassAnnos(className, file, enclosingClass);
AField field = classAnnos.fields.getVivify(fieldName);
field.setTypeMirror(lhsATM.getUnderlyingType());
return field.type;
}
@Override
public ATypeElement 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
*/
@SuppressWarnings("UnusedVariable")
private ATypeElement getPreconditionsForField(
ExecutableElement methodElement,
VariableElement fieldElement,
AnnotatedTypeFactory atypeFactory) {
AMethod methodAnnos = getMethodAnnos(methodElement);
TypeMirror typeMirror = TypeAnnotationUtils.unannotatedType(fieldElement.asType());
return methodAnnos.vivifyAndAddTypeMirrorToPrecondition(fieldElement, typeMirror).type;
}
/**
* 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
*/
@SuppressWarnings("UnusedVariable")
private ATypeElement getPostconditionsForField(
ExecutableElement methodElement,
VariableElement fieldElement,
AnnotatedTypeFactory atypeFactory) {
AMethod methodAnnos = getMethodAnnos(methodElement);
TypeMirror typeMirror = TypeAnnotationUtils.unannotatedType(fieldElement.asType());
return methodAnnos.vivifyAndAddTypeMirrorToPostcondition(fieldElement, typeMirror).type;
}
@Override
public boolean addMethodDeclarationAnnotation(
ExecutableElement methodElt, AnnotationMirror anno) {
// Do not infer types for library code, only for type-checked source code.
if (!ElementUtils.isElementFromSourceCode(methodElt)) {
return false;
}
AMethod methodAnnos = getMethodAnnos(methodElt);
scenelib.annotations.Annotation sceneAnno =
AnnotationConverter.annotationMirrorToAnnotation(anno);
boolean isNewAnnotation = methodAnnos.tlAnnotationsHere.add(sceneAnno);
return isNewAnnotation;
}
/**
* Write all modified scenes into files. (Scenes are modified by the method {@link
* #updateAnnotationSetInScene}.)
*
* @param outputFormat the output format to use when writing files
* @param checker the checker from which this method is called, for naming stub files
*/
public void writeScenes(OutputFormat outputFormat, BaseTypeChecker checker) {
// Create WPI directory if it doesn't exist already.
File jaifDir = new File(JAIF_FILES_PATH);
if (!jaifDir.exists()) {
jaifDir.mkdirs();
}
// Write scenes into files.
for (String jaifPath : modifiedScenes) {
scenes.get(jaifPath).writeToFile(jaifPath, annosToIgnore, outputFormat, checker);
}
modifiedScenes.clear();
}
/**
* Returns the String representing the .jaif path of a class given its name.
*
* @param className the simple name of a class
* @return the path to the .jaif file
*/
protected String getJaifPath(String className) {
String jaifPath = JAIF_FILES_PATH + className + ".jaif";
return jaifPath;
}
/**
* Reads a Scene from the given .jaif file, or returns an empty Scene if the file does not exist.
*
* @param jaifPath the .jaif file
* @return the Scene read from the file, or an empty Scene if the file does not exist
*/
private ASceneWrapper getScene(String jaifPath) {
AScene scene;
if (!scenes.containsKey(jaifPath)) {
File jaifFile = new File(jaifPath);
scene = new AScene();
if (jaifFile.exists()) {
try {
IndexFileParser.parseFile(jaifPath, scene);
} catch (IOException e) {
throw new UserError("Problem while reading %s: %s", jaifPath, e.getMessage());
}
}
ASceneWrapper wrapper = new ASceneWrapper(scene);
scenes.put(jaifPath, wrapper);
return wrapper;
} else {
return scenes.get(jaifPath);
}
}
/**
* Returns the scene-lib representation of the given className in the scene identified by the
* given jaifPath.
*
* @param className the name of the class to get, in binary form
* @param jaifPath the path to the jaif file that would represent that class (must end in ".jaif")
* @param classSymbol optionally, the ClassSymbol representing the class. Used to set the symbol
* information stored on an AClass.
* @return a version of the scene-lib representation of the class, augmented with symbol
* information if {@code classSymbol} was non-null
*/
protected AClass getAClass(
@BinaryName String className, String jaifPath, @Nullable ClassSymbol classSymbol) {
// Possibly reads .jaif file to obtain a Scene.
ASceneWrapper scene = getScene(jaifPath);
AClass aClass = scene.getAScene().classes.getVivify(className);
scene.updateSymbolInformation(aClass, classSymbol);
return aClass;
}
/**
* Returns the scene-lib representation of the given className in the scene identified by the
* given jaifPath.
*
* @param className the name of the class to get, in binary form
* @param jaifPath the path to the jaif file that would represent that class (must end in ".jaif")
* @return the scene-lib representation of the class, possibly augmented with symbol information
* if {@link #getAClass(String, String, com.sun.tools.javac.code.Symbol.ClassSymbol)} has
* already been called with a non-null third argument
*/
protected AClass getAClass(@BinaryName String className, String jaifPath) {
return getAClass(className, jaifPath, null);
}
/**
* Updates the set of annotations in a location of a Scene, as the result of a pseudo-assignment.
*
* <ul>
* <li>If there was no previous annotation for that location, then the updated set will be the
* annotations in rhsATM.
* <li>If there was a previous annotation, the updated set will be the LUB between the previous
* annotation and rhsATM.
* </ul>
*
* @param type ATypeElement of the Scene which will be modified
* @param jaifPath path to a .jaif file for a Scene; used for marking the scene as modified
* (needing to be written to disk)
* @param rhsATM the RHS of the annotated type on the source code
* @param lhsATM the LHS of the annotated type on the source code
* @param defLoc the location where the annotation will be added
* @param ignoreIfAnnotated if true, don't update any type that is explicitly annotated in the
* source code
*/
protected void updateAnnotationSetInScene(
ATypeElement type,
TypeUseLocation defLoc,
AnnotatedTypeMirror rhsATM,
AnnotatedTypeMirror lhsATM,
String jaifPath,
boolean ignoreIfAnnotated) {
if (rhsATM instanceof AnnotatedNullType && ignoreNullAssignments) {
return;
}
AnnotatedTypeMirror atmFromScene = atmFromStorageLocation(rhsATM.getUnderlyingType(), type);
updateAtmWithLub(rhsATM, atmFromScene);
if (lhsATM instanceof AnnotatedTypeVariable) {
Set<AnnotationMirror> upperAnnos =
((AnnotatedTypeVariable) lhsATM).getUpperBound().getEffectiveAnnotations();
// If the inferred type is a subtype of the upper bounds of the
// current type on the source code, halt.
if (upperAnnos.size() == rhsATM.getAnnotations().size()
&& atypeFactory.getQualifierHierarchy().isSubtype(rhsATM.getAnnotations(), upperAnnos)) {
return;
}
}
updateTypeElementFromATM(type, 1, defLoc, rhsATM, lhsATM, ignoreIfAnnotated);
modifiedScenes.add(jaifPath);
}
/**
* Updates sourceCodeATM to contain the LUB between sourceCodeATM and jaifATM, ignoring missing
* AnnotationMirrors from jaifATM -- it considers the LUB between an AnnotationMirror am and a
* missing AnnotationMirror to be am. The results are stored in sourceCodeATM.
*
* @param sourceCodeATM the annotated type on the source code
* @param jaifATM the annotated type on the .jaif file
*/
private void updateAtmWithLub(AnnotatedTypeMirror sourceCodeATM, AnnotatedTypeMirror jaifATM) {
switch (sourceCodeATM.getKind()) {
case TYPEVAR:
updateAtmWithLub(
((AnnotatedTypeVariable) sourceCodeATM).getLowerBound(),
((AnnotatedTypeVariable) jaifATM).getLowerBound());
updateAtmWithLub(
((AnnotatedTypeVariable) sourceCodeATM).getUpperBound(),
((AnnotatedTypeVariable) jaifATM).getUpperBound());
break;
// case WILDCARD:
// Because inferring type arguments is not supported, wildcards won't be encoutered
// updateAtmWithLub(((AnnotatedWildcardType)
// sourceCodeATM).getExtendsBound(),
// ((AnnotatedWildcardType)
// jaifATM).getExtendsBound());
// updateAtmWithLub(((AnnotatedWildcardType)
// sourceCodeATM).getSuperBound(),
// ((AnnotatedWildcardType) jaifATM).getSuperBound());
// break;
case ARRAY:
updateAtmWithLub(
((AnnotatedArrayType) sourceCodeATM).getComponentType(),
((AnnotatedArrayType) jaifATM).getComponentType());
break;
// case DECLARED:
// inferring annotations on type arguments is not supported, so no need to recur on
// generic types. If this was every implemented, this method would need VisitHistory
// object to prevent infinite recursion on types such as T extends List<T>.
default:
// ATM only has primary annotations
break;
}
// LUB primary annotations
Set<AnnotationMirror> annosToReplace = new HashSet<>(sourceCodeATM.getAnnotations().size());
for (AnnotationMirror amSource : sourceCodeATM.getAnnotations()) {
AnnotationMirror amJaif = jaifATM.getAnnotationInHierarchy(amSource);
// amJaif only contains annotations from the jaif, so it might be missing
// an annotation in the hierarchy
if (amJaif != null) {
amSource = atypeFactory.getQualifierHierarchy().leastUpperBound(amSource, amJaif);
}
annosToReplace.add(amSource);
}
sourceCodeATM.replaceAnnotations(annosToReplace);
}
/**
* Returns true if {@code am} should not be inserted in source code, for example {@link
* org.checkerframework.common.value.qual.BottomVal}. This happens when {@code am} cannot be
* inserted in source code or is the default for the location passed as argument.
*
* <p>Invisible qualifiers, which are annotations that contain the {@link
* org.checkerframework.framework.qual.InvisibleQualifier} meta-annotation, also return true.
*
* <p>TODO: Merge functionality somewhere else with {@link
* org.checkerframework.framework.util.defaults.QualifierDefaults}. Look into the
* createQualifierDefaults method in {@link GenericAnnotatedTypeFactory} (which uses the
* QualifierDefaults class linked above) before changing anything here. See
* https://github.com/typetools/checker-framework/issues/683 .
*
* @param am an annotation to test for whether it should be inserted into source code
* @param location where the location would be inserted; used to determine if {@code am} is the
* default for that location
* @param atm its kind is used to determine if {@code am} is the default for that kind
* @return true if am should not be inserted into source code, or if am is invisible
*/
private boolean shouldIgnore(
AnnotationMirror am, TypeUseLocation location, AnnotatedTypeMirror atm) {
Element elt = am.getAnnotationType().asElement();
// Checks if am is an implementation detail (a type qualifier used
// internally by the type system and not meant to be seen by the user).
Target target = elt.getAnnotation(Target.class);
if (target != null && target.value().length == 0) {
return true;
}
if (elt.getAnnotation(InvisibleQualifier.class) != null) {
return true;
}
// Checks if am is default
if (elt.getAnnotation(DefaultQualifierInHierarchy.class) != null) {
return true;
}
DefaultQualifier defaultQual = elt.getAnnotation(DefaultQualifier.class);
if (defaultQual != null) {
for (TypeUseLocation loc : defaultQual.locations()) {
if (loc == TypeUseLocation.ALL || loc == location) {
return true;
}
}
}
DefaultFor defaultQualForLocation = elt.getAnnotation(DefaultFor.class);
if (defaultQualForLocation != null) {
for (TypeUseLocation loc : defaultQualForLocation.value()) {
if (loc == TypeUseLocation.ALL || loc == location) {
return true;
}
}
}
// Checks if am is a default annotation.
// This case checks if it is meta-annotated with @DefaultFor.
// TODO: Handle cases of annotations added via an
// org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator.
DefaultFor defaultFor = elt.getAnnotation(DefaultFor.class);
if (defaultFor != null) {
org.checkerframework.framework.qual.TypeKind[] types = defaultFor.typeKinds();
TypeKind atmKind = atm.getUnderlyingType().getKind();
if (hasMatchingTypeKind(atmKind, types)) {
return true;
}
}
return false;
}
/** Returns true, iff a matching TypeKind is found. */
private boolean hasMatchingTypeKind(
TypeKind atmKind, org.checkerframework.framework.qual.TypeKind[] types) {
for (org.checkerframework.framework.qual.TypeKind tk : types) {
if (tk.name().equals(atmKind.name())) {
return true;
}
}
return false;
}
/**
* Returns a subset of annosSet, consisting of the annotations supported by the type factory
* associated with this. These are not necessarily legal annotations: they have the right name,
* but they may lack elements (fields).
*
* @param annosSet a set of annotations
* @return the annoattions supported by this object's AnnotatedTypeFactory
*/
private Set<Annotation> getSupportedAnnosInSet(Set<Annotation> annosSet) {
Set<Annotation> output = new HashSet<>(1);
Set<Class<? extends java.lang.annotation.Annotation>> supportedAnnos =
atypeFactory.getSupportedTypeQualifiers();
for (Annotation anno : annosSet) {
for (Class<? extends java.lang.annotation.Annotation> clazz : supportedAnnos) {
// TODO: Remove comparison by name, and make this routine more efficient.
if (clazz.getName().equals(anno.def.name)) {
output.add(anno);
}
}
}
return output;
}
@Override
public AnnotatedTypeMirror atmFromStorageLocation(
TypeMirror typeMirror, ATypeElement storageLocation) {
AnnotatedTypeMirror result = AnnotatedTypeMirror.createType(typeMirror, atypeFactory, false);
updateAtmFromATypeElement(result, storageLocation);
return result;
}
/**
* Updates an {@link org.checkerframework.framework.type.AnnotatedTypeMirror} to contain the
* {@link scenelib.annotations.Annotation}s of an {@link scenelib.annotations.el.ATypeElement}.
*
* @param result the AnnotatedTypeMirror to be modified
* @param storageLocation the {@link scenelib.annotations.el.ATypeElement} used
*/
private void updateAtmFromATypeElement(AnnotatedTypeMirror result, ATypeElement storageLocation) {
Set<Annotation> annos = getSupportedAnnosInSet(storageLocation.tlAnnotationsHere);
for (Annotation anno : annos) {
AnnotationMirror am =
AnnotationConverter.annotationToAnnotationMirror(anno, atypeFactory.getProcessingEnv());
result.addAnnotation(am);
}
if (result.getKind() == TypeKind.ARRAY) {
AnnotatedArrayType aat = (AnnotatedArrayType) result;
for (ATypeElement innerType : storageLocation.innerTypes.values()) {
updateAtmFromATypeElement(aat.getComponentType(), innerType);
}
}
if (result.getKind() == TypeKind.TYPEVAR) {
AnnotatedTypeVariable atv = (AnnotatedTypeVariable) result;
for (ATypeElement innerType : storageLocation.innerTypes.values()) {
updateAtmFromATypeElement(atv.getUpperBound(), innerType);
}
}
}
@Override
public void updateStorageLocationFromAtm(
AnnotatedTypeMirror newATM,
AnnotatedTypeMirror curATM,
ATypeElement typeToUpdate,
TypeUseLocation defLoc,
boolean ignoreIfAnnotated) {
updateTypeElementFromATM(typeToUpdate, 1, defLoc, newATM, curATM, ignoreIfAnnotated);
}
///
/// 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 prepareSceneForWriting(AScene compilationUnitAnnos) {
for (Map.Entry<String, AClass> classEntry : compilationUnitAnnos.classes.entrySet()) {
prepareClassForWriting(classEntry.getValue());
}
}
/**
* 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(AClass classAnnos) {
for (Map.Entry<String, AMethod> methodEntry : classAnnos.methods.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(AMethod methodAnnos) {
atypeFactory.prepareMethodForWriting(methodAnnos);
}
@Override
public void writeResultsToFile(
WholeProgramInference.OutputFormat outputFormat, BaseTypeChecker checker) {
if (outputFormat == OutputFormat.AJAVA) {
throw new BugInCF("WholeProgramInferenceScenes used with format " + outputFormat);
}
for (String file : modifiedScenes) {
ASceneWrapper scene = scenes.get(file);
prepareSceneForWriting(scene.getAScene());
}
writeScenes(outputFormat, checker);
}
@Override
public void setFileModified(String path) {
modifiedScenes.add(path);
}
@Override
public void preprocessClassTree(ClassTree classTree) {
// This implementation does nothing.
}
/**
* Updates an {@link scenelib.annotations.el.ATypeElement} to have the annotations of an {@link
* org.checkerframework.framework.type.AnnotatedTypeMirror} passed as argument. Annotations in the
* original set that should be ignored (see {@link #shouldIgnore}) are not added to the resulting
* set. This method also checks if the AnnotatedTypeMirror has explicit annotations in source
* code, and if that is the case no annotations are added for that location.
*
* <p>This method removes from the ATypeElement all annotations supported by this object's
* AnnotatedTypeFactory before inserting new ones. It is assumed that every time this method is
* called, the AnnotatedTypeMirror has a better type estimate for the ATypeElement. Therefore, it
* is not a problem to remove all annotations before inserting the new annotations.
*
* @param typeToUpdate the ATypeElement that will be updated
* @param idx used to write annotations on compound types of an ATypeElement
* @param defLoc the location where the annotation will be added
* @param newATM the AnnotatedTypeMirror whose annotations will be added to the ATypeElement
* @param curATM used to check if the element which will be updated has explicit annotations in
* source code
* @param ignoreIfAnnotated if true, don't update any type that is explicitly annotated in the
* source code
*/
private void updateTypeElementFromATM(
ATypeElement typeToUpdate,
int idx,
TypeUseLocation defLoc,
AnnotatedTypeMirror newATM,
AnnotatedTypeMirror curATM,
boolean ignoreIfAnnotated) {
// Clears only the annotations that are supported by the relevant AnnotatedTypeFactory.
// The others stay intact.
if (idx == 1) {
// This if avoids clearing the annotations multiple times in cases
// of type variables and compound types.
Set<Annotation> annosToRemove = getSupportedAnnosInSet(typeToUpdate.tlAnnotationsHere);
// This method may be called consecutive times for the same ATypeElement. Each time it is
// called, the AnnotatedTypeMirror has a better type estimate for the ATypeElement. Therefore,
// it is not a problem to remove all annotations before inserting the new annotations.
typeToUpdate.tlAnnotationsHere.removeAll(annosToRemove);
}
// Only update the ATypeElement if there are no explicit annotations.
if (curATM.getExplicitAnnotations().isEmpty() || !ignoreIfAnnotated) {
for (AnnotationMirror am : newATM.getAnnotations()) {
addAnnotationsToATypeElement(
newATM, typeToUpdate, defLoc, am, curATM.hasEffectiveAnnotation(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. #shouldIgnore prevent annotations that are subtypes of type
// vars upper bound from being inserted.
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;
}
addAnnotationsToATypeElement(
newATM, typeToUpdate, defLoc, am, curATM.hasEffectiveAnnotation(am));
}
}
// Recursively update compound type and type variable type if they exist.
if (newATM.getKind() == TypeKind.ARRAY && curATM.getKind() == TypeKind.ARRAY) {
AnnotatedArrayType newAAT = (AnnotatedArrayType) newATM;
AnnotatedArrayType oldAAT = (AnnotatedArrayType) curATM;
updateTypeElementFromATM(
typeToUpdate.innerTypes.getVivify(
TypePathEntry.getTypePathEntryListFromBinary(Collections.nCopies(2 * idx, 0))),
idx + 1,
defLoc,
newAAT.getComponentType(),
oldAAT.getComponentType(),
ignoreIfAnnotated);
}
}
private void addAnnotationsToATypeElement(
AnnotatedTypeMirror newATM,
ATypeElement typeToUpdate,
TypeUseLocation defLoc,
AnnotationMirror am,
boolean isEffectiveAnnotation) {
Annotation anno = AnnotationConverter.annotationMirrorToAnnotation(am);
typeToUpdate.tlAnnotationsHere.add(anno);
if (isEffectiveAnnotation || shouldIgnore(am, defLoc, newATM)) {
// firstKey works as a unique identifier for each annotation
// that should not be inserted in source code
String firstKey = aTypeElementToString(typeToUpdate);
Pair<String, TypeUseLocation> key = Pair.of(firstKey, defLoc);
Set<String> annosIgnored = annosToIgnore.get(key);
if (annosIgnored == null) {
annosIgnored = new HashSet<>();
annosToIgnore.put(key, annosIgnored);
}
annosIgnored.add(anno.def().toString());
}
}
/**
* Returns a string representation of an ATypeElement, for use as part of a key in {@link
* AnnotationsInContexts}.
*
* @param aType an ATypeElement to convert to a string representation
* @return a string representation of the argument
*/
public static String aTypeElementToString(ATypeElement aType) {
// return aType.description.toString() + aType.tlAnnotationsHere;
return aType.description.toString();
}
/**
* Maps the {@link #aTypeElementToString} representation of an ATypeElement and its
* TypeUseLocation to a set of names of annotations.
*/
public static class AnnotationsInContexts
extends HashMap<Pair<String, TypeUseLocation>, Set<String>> {
private static final long serialVersionUID = 20200321L;
}
/**
* Returns the "flatname" of the class enclosing {@code localVariableNode}.
*
* @param localVariableNode the {@link LocalVariableNode}
* @return the "flatname" of the class enclosing {@code localVariableNode}
*/
private static @BinaryName String getEnclosingClassName(LocalVariableNode localVariableNode) {
return ElementUtils.getBinaryName(
ElementUtils.enclosingTypeElement(localVariableNode.getElement()));
}
}