| package org.checkerframework.common.wholeprograminference.scenelib; |
| |
| import com.sun.tools.javac.code.Symbol.ClassSymbol; |
| import java.io.File; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| 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.ElementKind; |
| import javax.lang.model.element.TypeElement; |
| import javax.lang.model.element.VariableElement; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| import org.checkerframework.common.basetype.BaseTypeChecker; |
| import org.checkerframework.common.wholeprograminference.AnnotationConverter; |
| import org.checkerframework.common.wholeprograminference.SceneToStubWriter; |
| import org.checkerframework.common.wholeprograminference.WholeProgramInference.OutputFormat; |
| import org.checkerframework.common.wholeprograminference.WholeProgramInferenceScenesStorage; |
| import org.checkerframework.common.wholeprograminference.WholeProgramInferenceScenesStorage.AnnotationsInContexts; |
| import org.checkerframework.framework.qual.TypeUseLocation; |
| import org.checkerframework.javacutil.BugInCF; |
| import org.checkerframework.javacutil.ElementUtils; |
| import org.checkerframework.javacutil.Pair; |
| import org.checkerframework.javacutil.UserError; |
| import org.plumelib.util.CollectionsPlume; |
| 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.DefException; |
| import scenelib.annotations.io.IndexFileWriter; |
| |
| /** |
| * scene-lib (from the Annotation File Utilities) doesn't provide enough information to usefully |
| * print stub files: it lacks information about what is and is not an enum, about the base types of |
| * variables, and about formal parameter names. |
| * |
| * <p>This class wraps AScene but provides access to that missing information. This allows us to |
| * preserve the code that generates .jaif files, while allowing us to sanely and safely keep the |
| * information we need to generate stubs. |
| * |
| * <p>This class would be better as a subclass of AScene. |
| */ |
| public class ASceneWrapper { |
| |
| /** The AScene being wrapped. */ |
| private final AScene theScene; |
| |
| /** |
| * Constructor. Pass the AScene to wrap. |
| * |
| * @param theScene the scene to wrap |
| */ |
| public ASceneWrapper(AScene theScene) { |
| this.theScene = theScene; |
| } |
| |
| /** |
| * Removes the specified annotations from an AScene. |
| * |
| * @param scene the scene from which to remove annotations |
| * @param annosToRemove annotations that should not be added to .jaif or stub files |
| */ |
| private void removeAnnosFromScene(AScene scene, AnnotationsInContexts annosToRemove) { |
| for (AClass aclass : scene.classes.values()) { |
| for (AField field : aclass.fields.values()) { |
| removeAnnosFromATypeElement(field.type, TypeUseLocation.FIELD, annosToRemove); |
| } |
| for (AMethod method : aclass.methods.values()) { |
| removeAnnosFromATypeElement(method.returnType, TypeUseLocation.RETURN, annosToRemove); |
| removeAnnosFromATypeElement(method.receiver.type, TypeUseLocation.RECEIVER, annosToRemove); |
| for (AField param : method.parameters.values()) { |
| removeAnnosFromATypeElement(param.type, TypeUseLocation.PARAMETER, annosToRemove); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Removes the specified annotations from an ATypeElement. |
| * |
| * @param typeElt the type element from which to remove annotations |
| * @param loc the location where typeElt is used |
| * @param annosToRemove annotations that should not be added to .jaif or stub files |
| */ |
| private void removeAnnosFromATypeElement( |
| ATypeElement typeElt, TypeUseLocation loc, AnnotationsInContexts annosToRemove) { |
| String annosToRemoveKey = WholeProgramInferenceScenesStorage.aTypeElementToString(typeElt); |
| Set<String> annosToRemoveForLocation = annosToRemove.get(Pair.of(annosToRemoveKey, loc)); |
| if (annosToRemoveForLocation != null) { |
| Set<Annotation> annosToRemoveHere = new HashSet<>(); |
| for (Annotation anno : typeElt.tlAnnotationsHere) { |
| if (annosToRemoveForLocation.contains(anno.def().toString())) { |
| annosToRemoveHere.add(anno); |
| } |
| } |
| typeElt.tlAnnotationsHere.removeAll(annosToRemoveHere); |
| } |
| |
| // Recursively remove annotations from inner types |
| for (ATypeElement innerType : typeElt.innerTypes.values()) { |
| removeAnnosFromATypeElement(innerType, loc, annosToRemove); |
| } |
| } |
| |
| /** |
| * Write the scene wrapped by this object to a file at the given path. |
| * |
| * @param jaifPath the path of the file to be written, but ending in ".jaif". If {@code |
| * outputformat} is not {@code JAIF}, the path will be modified to match. |
| * @param annosToIgnore which annotations should be ignored in which contexts |
| * @param outputFormat the output format to use |
| * @param checker the checker from which this method is called, for naming stub files |
| */ |
| public void writeToFile( |
| String jaifPath, |
| AnnotationsInContexts annosToIgnore, |
| OutputFormat outputFormat, |
| BaseTypeChecker checker) { |
| assert jaifPath.endsWith(".jaif"); |
| AScene scene = theScene.clone(); |
| removeAnnosFromScene(scene, annosToIgnore); |
| scene.prune(); |
| String filepath; |
| switch (outputFormat) { |
| case JAIF: |
| filepath = jaifPath; |
| break; |
| case STUB: |
| String astubWithChecker = "-" + checker.getClass().getCanonicalName() + ".astub"; |
| filepath = jaifPath.replace(".jaif", astubWithChecker); |
| break; |
| default: |
| throw new BugInCF("Unhandled outputFormat " + outputFormat); |
| } |
| new File(filepath).delete(); |
| // Only write non-empty scenes into files. |
| if (!scene.isEmpty()) { |
| try { |
| switch (outputFormat) { |
| case STUB: |
| // For stub files, pass in the checker to compute contracts on the fly; precomputing |
| // yields incorrect annotations, most likely due to nested classes. |
| SceneToStubWriter.write(this, filepath, checker); |
| break; |
| case JAIF: |
| // For .jaif files, precompute contracts because the Annotation File Utilities knows |
| // nothing about (and cannot depend on) the Checker Framework. |
| for (Map.Entry<String, AClass> classEntry : scene.classes.entrySet()) { |
| AClass aClass = classEntry.getValue(); |
| for (Map.Entry<String, AMethod> methodEntry : aClass.getMethods().entrySet()) { |
| AMethod aMethod = methodEntry.getValue(); |
| List<AnnotationMirror> contractAnnotationMirrors = |
| checker.getTypeFactory().getContractAnnotations(aMethod); |
| List<Annotation> contractAnnotations = |
| CollectionsPlume.mapList( |
| AnnotationConverter::annotationMirrorToAnnotation, |
| contractAnnotationMirrors); |
| aMethod.contracts = contractAnnotations; |
| } |
| } |
| IndexFileWriter.write(scene, new FileWriter(filepath)); |
| break; |
| default: |
| throw new BugInCF("Unhandled outputFormat " + outputFormat); |
| } |
| } catch (IOException e) { |
| throw new UserError("Problem while writing %s: %s", filepath, e.getMessage()); |
| } catch (DefException e) { |
| throw new BugInCF(e); |
| } |
| } |
| } |
| |
| /** |
| * Updates the symbol information stored in AClass for the given class. May be called multiple |
| * times (and needs to be if the second parameter was null the first time it was called; only some |
| * calls provide the symbol information). |
| * |
| * @param aClass the class representation in which the symbol information is to be updated |
| * @param classSymbol the source of the symbol information; may be null, in which case this method |
| * does nothing |
| */ |
| public void updateSymbolInformation(AClass aClass, @Nullable ClassSymbol classSymbol) { |
| if (classSymbol == null) { |
| return; |
| } |
| if (classSymbol.isEnum()) { |
| List<VariableElement> enumConstants = new ArrayList<>(); |
| for (Element e : ((TypeElement) classSymbol).getEnclosedElements()) { |
| if (e.getKind() == ElementKind.ENUM_CONSTANT) { |
| enumConstants.add((VariableElement) e); |
| } |
| } |
| if (!aClass.isEnum(classSymbol.getSimpleName().toString())) { |
| aClass.setEnumConstants(enumConstants); |
| } else { |
| // Verify that the existing value is consistent. |
| List<VariableElement> existingEnumConstants = aClass.getEnumConstants(); |
| if (existingEnumConstants.size() != enumConstants.size()) { |
| throw new BugInCF( |
| "inconsistent enum constants in WPI for class " |
| + classSymbol.getQualifiedName().toString()); |
| } |
| for (int i = 0; i < enumConstants.size(); i++) { |
| if (!existingEnumConstants.get(i).equals(enumConstants.get(i))) { |
| throw new BugInCF( |
| "inconsistent enum constants in WPI for class " |
| + classSymbol.getQualifiedName().toString()); |
| } |
| } |
| } |
| } |
| |
| ClassSymbol outerClass = classSymbol; |
| ClassSymbol previous = classSymbol; |
| do { |
| if (outerClass.isEnum()) { |
| aClass.markAsEnum(outerClass.getSimpleName().toString()); |
| } |
| Element element = classSymbol.getEnclosingElement(); |
| if (element == null || element.getKind() == ElementKind.PACKAGE) { |
| break; |
| } |
| TypeElement t = ElementUtils.enclosingTypeElement(element); |
| previous = outerClass; |
| outerClass = (ClassSymbol) t; |
| // It is necessary to check that previous isn't equal to outer class because |
| // otherwise this loop will sometimes run forever. |
| } while (outerClass != null && !previous.equals(outerClass)); |
| |
| aClass.setTypeElement(classSymbol); |
| } |
| |
| /** |
| * Avoid using this if possible; use the other methods of this class unless you absolutely need an |
| * AScene. |
| * |
| * @return the AScene representation of this |
| */ |
| public AScene getAScene() { |
| return theScene; |
| } |
| } |