blob: efdccc2424a53164693eb25dab0c8b9f2e810f20 [file] [log] [blame]
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;
}
}