package org.checkerframework.common.wholeprograminference; import annotations.Annotation; import annotations.el.AClass; import annotations.el.AField; import annotations.el.AMethod; import annotations.el.AScene; import annotations.el.ATypeElement; import annotations.el.DefException; import annotations.el.InnerTypeLocation; import annotations.io.IndexFileParser; import annotations.io.IndexFileWriter; import com.sun.tools.javac.code.TypeAnnotationPosition; import java.io.File; import java.io.FileWriter; 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.type.MirroredTypesException; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.framework.qual.DefaultFor; import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.ImplicitFor; 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.javacutil.ErrorReporter; import org.checkerframework.javacutil.Pair; /** * This class stores annotations for fields, method return types, and method parameters. * * <p>The set of annotations inferred for a certain class is stored in an {@link * annotations.el.AScene}, which {@link #writeScenesToJaif} can write into a .jaif file. For * example, a class field of a class whose fully-qualified name is {@code my.package.MyClass} will * have its inferred type stored in a Scene, and later written into a file named {@code * my.package.MyClass.jaif}. * * <p>This class populates the initial Scenes by reading existing .jaif files on the {@link * #jaifFilesPath} directory. Having more information in those initial .jaif files means that the * precision achieved by the whole-program inference analysis will be better. {@link * #writeScenesToJaif} rewrites the initial .jaif files, and may create new ones. */ public class WholeProgramInferenceScenesHelper { /** * Maps the toString() representation of an ATypeElement and its TypeUseLocation to a set of * names of annotations that should not be added to .jaif files for that location. */ private final Map<Pair<String, TypeUseLocation>, Set<String>> annosToIgnore = new HashMap<>(); /** * 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 jaifFilesPath = "build" + File.separator + "whole-program-inference" + File.separator; /** Indicates whether assignments where the rhs is null should be ignored. */ private final boolean ignoreNullAssignments; /** Maps .jaif file paths (Strings) to Scenes. Relatives to jaifFilesPath. */ private final Map<String, AScene> scenes = new HashMap<>(); /** * Set representing Scenes that were modified since the last time all Scenes were written into * .jaif files. Each String element of this set is a 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}.) */ private final Set<String> modifiedScenes = new HashSet<>(); public WholeProgramInferenceScenesHelper(boolean ignoreNullAssignments) { this.ignoreNullAssignments = ignoreNullAssignments; } /** * Write all modified scenes into .jaif files. (Scenes are modified by the method {@link * #updateAnnotationSetInScene}.) */ public void writeScenesToJaif() { // Create .jaif files directory if it doesn't exist already. File jaifDir = new File(jaifFilesPath); if (!jaifDir.exists()) { jaifDir.mkdirs(); } // Write scenes into .jaif files. for (String jaifPath : modifiedScenes) { try { AScene scene = scenes.get(jaifPath).clone(); removeIgnoredAnnosFromScene(scene); new File(jaifPath).delete(); if (!scene.prune()) { // Only write non-empty scenes into .jaif files. IndexFileWriter.write(scene, new FileWriter(jaifPath)); } } catch (IOException e) { ErrorReporter.errorAbort( "Problem while reading file in: " + jaifPath + ". Exception message: " + e.getMessage(), e); } catch (DefException e) { ErrorReporter.errorAbort(e.getMessage(), e); } } modifiedScenes.clear(); } /** Returns the String representing the .jaif path of a class given its name. */ protected String getJaifPath(String className) { String jaifPath = jaifFilesPath + className + ".jaif"; return jaifPath; } /** * Returns the Scene stored in a .jaif file path passed as input. If the file does not exist, an * empty Scene is created. */ protected AScene 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) { ErrorReporter.errorAbort( "Problem while reading file in: " + jaifPath + "." + " Exception message: " + e.getMessage(), e); } } scenes.put(jaifPath, scene); } else { scene = scenes.get(jaifPath); } return scene; } /** Returns the AClass in an AScene, given a className and a jaifPath. */ protected AClass getAClass(String className, String jaifPath) { // Possibly reads .jaif file to obtain a Scene. AScene scene = getScene(jaifPath); return scene.classes.vivify(className); } /** * Updates the set of annotations in a location of a Scene. * * <ul> * <li>If there was no previous annotation for that location, then the updated set will be the * annotations in newATM. * <li>If there was a previous annotation, the updated set will be the LUB between the * previous annotation and newATM. * </ul> * * <p> * * @param type ATypeElement of the Scene which will be modified * @param atf the annotated type factory of a given type system, whose type hierarchy will be * used * @param jaifPath used to identify a Scene * @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 */ protected void updateAnnotationSetInScene( ATypeElement type, AnnotatedTypeFactory atf, String jaifPath, AnnotatedTypeMirror rhsATM, AnnotatedTypeMirror lhsATM, TypeUseLocation defLoc) { if (rhsATM instanceof AnnotatedNullType && ignoreNullAssignments) { return; } AnnotatedTypeMirror atmFromJaif = AnnotatedTypeMirror.createType(rhsATM.getUnderlyingType(), atf, false); typeElementToATM(atmFromJaif, type, atf); updatesATMWithLUB(atf, rhsATM, atmFromJaif); 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() && atf.getQualifierHierarchy().isSubtype(rhsATM.getAnnotations(), upperAnnos)) { return; } } updateTypeElementFromATM(rhsATM, lhsATM, atf, type, 1, defLoc); modifiedScenes.add(jaifPath); } /** * Removes all annotations that should be ignored from an AScene. (See {@link #shouldIgnore}). */ private void removeIgnoredAnnosFromScene(AScene scene) { for (AClass aclass : scene.classes.values()) { for (AField field : aclass.fields.values()) { removeIgnoredAnnosFromATypeElement(field.type, TypeUseLocation.FIELD); } for (AMethod method : aclass.methods.values()) { // Return type removeIgnoredAnnosFromATypeElement(method.returnType, TypeUseLocation.RETURN); // Receiver type removeIgnoredAnnosFromATypeElement(method.receiver.type, TypeUseLocation.RECEIVER); // Parameter type for (AField param : method.parameters.values()) { removeIgnoredAnnosFromATypeElement(param.type, TypeUseLocation.PARAMETER); } } } } /** * Removes all annotations that should be ignored from an ATypeElement. (See {@link * #shouldIgnore}). */ private void removeIgnoredAnnosFromATypeElement(ATypeElement typeEl, TypeUseLocation loc) { Set<Annotation> annosToRemove = new HashSet<>(); Set<String> annosToIgnoreForLocation = annosToIgnore.get(Pair.of(typeEl.toString(), loc)); if (annosToIgnoreForLocation == null) { // No annotations to ignore for that position. return; } for (Annotation anno : typeEl.tlAnnotationsHere) { if (annosToIgnoreForLocation.contains(anno.def().toString())) { annosToRemove.add(anno); } } typeEl.tlAnnotationsHere.removeAll(annosToRemove); // Remove annotations recursively for inner types. for (ATypeElement innerType : typeEl.innerTypes.values()) { removeIgnoredAnnosFromATypeElement(innerType, loc); } } /** * 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 atf the annotated type factory of a given type system, whose type hierarchy will be * used * @param sourceCodeATM the annotated type on the source code * @param jaifATM the annotated type on the .jaif file. */ private void updatesATMWithLUB( AnnotatedTypeFactory atf, AnnotatedTypeMirror sourceCodeATM, AnnotatedTypeMirror jaifATM) { switch (sourceCodeATM.getKind()) { case TYPEVAR: updatesATMWithLUB( atf, ((AnnotatedTypeVariable) sourceCodeATM).getLowerBound(), ((AnnotatedTypeVariable) jaifATM).getLowerBound()); updatesATMWithLUB( atf, ((AnnotatedTypeVariable) sourceCodeATM).getUpperBound(), ((AnnotatedTypeVariable) jaifATM).getUpperBound()); break; // case WILDCARD: // Because inferring type arguments is not supported, wildcards won't be encoutered // updatesATMWithLUB(atf, ((AnnotatedWildcardType) sourceCodeATM).getExtendsBound(), // ((AnnotatedWildcardType) jaifATM).getExtendsBound()); // updatesATMWithLUB(atf, ((AnnotatedWildcardType) sourceCodeATM).getSuperBound(), // ((AnnotatedWildcardType) jaifATM).getSuperBound()); // break; case ARRAY: updatesATMWithLUB( atf, ((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<>(); 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 = atf.getQualifierHierarchy().leastUpperBound(amSource, amJaif); } annosToReplace.add(amSource); } sourceCodeATM.replaceAnnotations(annosToReplace); } /** * Returns true if am should not be inserted in source code, for example {@link * org.checkerframework.common.value.qual.BottomVal}. This happens when 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.type.GenericAnnotatedTypeFactory#createQualifierDefaults}. * Look into the createQualifierDefaults method before changing anything here. See Issue 683 * https://github.com/typetools/checker-framework/issues/683 */ private boolean shouldIgnore( AnnotationMirror am, TypeUseLocation location, AnnotatedTypeFactory atf, 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 an implicit annotation // TODO: Handles cases of implicit annotations added via an // org.checkerframework.framework.type.treeannotator.ImplicitsTreeAnnotator ImplicitFor implicitFor = elt.getAnnotation(ImplicitFor.class); if (implicitFor != null) { TypeKind[] types = implicitFor.types(); TypeKind atmKind = atm.getUnderlyingType().getKind(); for (TypeKind tk : types) { if (tk == atmKind) return true; } try { Class<?>[] names = implicitFor.typeNames(); for (Class<?> c : names) { TypeMirror underlyingtype = atm.getUnderlyingType(); while (underlyingtype instanceof javax.lang.model.type.ArrayType) { underlyingtype = ((javax.lang.model.type.ArrayType) underlyingtype) .getComponentType(); } if (c.getCanonicalName().equals(atm.getUnderlyingType().toString())) { return true; } } } catch (MirroredTypesException e) { } } return false; } /** Returns a subset of annosSet, consisting of the annotations supported by atf. */ private Set<Annotation> getSupportedAnnosInSet( Set<Annotation> annosSet, AnnotatedTypeFactory atf) { Set<Annotation> output = new HashSet<>(); Set<Class<? extends java.lang.annotation.Annotation>> supportedAnnos = atf.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; } /** * Updates an {@link org.checkerframework.framework.type.AnnotatedTypeMirror} to contain the * {@link annotations.Annotation}s of an {@link annotations.el.ATypeElement}. * * @param atm the AnnotatedTypeMirror to be modified * @param type the {@link annotations.el.ATypeElement} * @param atf the annotated type factory of a given type system, whose type hierarchy will be * used */ private void typeElementToATM( AnnotatedTypeMirror atm, ATypeElement type, AnnotatedTypeFactory atf) { Set<Annotation> annos = getSupportedAnnosInSet(type.tlAnnotationsHere, atf); for (Annotation anno : annos) { AnnotationMirror am = AnnotationConverter.annotationToAnnotationMirror(anno, atf.getProcessingEnv()); atm.addAnnotation(am); } if (atm.getKind() == TypeKind.ARRAY) { AnnotatedArrayType aat = (AnnotatedArrayType) atm; for (ATypeElement innerType : type.innerTypes.values()) { typeElementToATM(aat.getComponentType(), innerType, atf); } } if (atm.getKind() == TypeKind.TYPEVAR) { AnnotatedTypeVariable atv = (AnnotatedTypeVariable) atm; for (ATypeElement innerType : type.innerTypes.values()) { typeElementToATM(atv.getUpperBound(), innerType, atf); } } } /** * Updates an {@link 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 atf 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 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 atf the annotated type factory of a given type system, whose type hierarchy will be * used * @param typeToUpdate the ATypeElement which 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 */ private void updateTypeElementFromATM( AnnotatedTypeMirror newATM, AnnotatedTypeMirror curATM, AnnotatedTypeFactory atf, ATypeElement typeToUpdate, int idx, TypeUseLocation defLoc) { // Clears only the annotations that are supported by atf. // 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, atf); // 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().size() == 0) { for (AnnotationMirror am : newATM.getAnnotations()) { addAnnotationsToATypeElement(newATM, atf, typeToUpdate, defLoc, 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, atf, typeToUpdate, defLoc, 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( newAAT.getComponentType(), oldAAT.getComponentType(), atf, typeToUpdate.innerTypes.vivify( new InnerTypeLocation( TypeAnnotationPosition.getTypePathFromBinary( Collections.nCopies(2 * idx, 0)))), idx + 1, defLoc); } } private void addAnnotationsToATypeElement( AnnotatedTypeMirror newATM, AnnotatedTypeFactory atf, ATypeElement typeToUpdate, TypeUseLocation defLoc, AnnotationMirror am) { Annotation anno = AnnotationConverter.annotationMirrorToAnnotation(am); if (anno != null) { typeToUpdate.tlAnnotationsHere.add(anno); if (shouldIgnore(am, defLoc, atf, newATM)) { Pair<String, TypeUseLocation> key = Pair.of(typeToUpdate.toString(), defLoc); Set<String> annosIgnored = annosToIgnore.get(key); if (annosIgnored == null) { annosIgnored = new HashSet<>(); annosToIgnore.put(key, annosIgnored); } annosIgnored.add(anno.def().toString()); } } } }