org.checkerframework.common.wholeprograminference.SceneToStubWriter Maven / Gradle / Ivy
Show all versions of checker Show documentation
package org.checkerframework.common.wholeprograminference;
import com.google.common.collect.ComparisonChain;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.afu.scenelib.Annotation;
import org.checkerframework.afu.scenelib.el.AClass;
import org.checkerframework.afu.scenelib.el.AField;
import org.checkerframework.afu.scenelib.el.AMethod;
import org.checkerframework.afu.scenelib.el.AScene;
import org.checkerframework.afu.scenelib.el.ATypeElement;
import org.checkerframework.afu.scenelib.el.AnnotationDef;
import org.checkerframework.afu.scenelib.el.DefCollector;
import org.checkerframework.afu.scenelib.el.DefException;
import org.checkerframework.afu.scenelib.el.TypePathEntry;
import org.checkerframework.afu.scenelib.field.AnnotationFieldType;
import org.checkerframework.checker.index.qual.SameLen;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.value.qual.MinLen;
import org.checkerframework.common.wholeprograminference.scenelib.ASceneWrapper;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
// In this file, "base name" means "type without its package part in binary name format".
// For example, "Outer$Inner" is a base name.
/**
* Static method {@link #write} writes an {@link AScene} to a file in stub file format. This class
* is the equivalent of {@code IndexFileWriter} from the Annotation File Utilities, but outputs the
* results in the stub file format instead of jaif format. This class is not part of the Annotation
* File Utilities, a library for manipulating .jaif files, because it has nothing to do with .jaif
* files.
*
* This class works by taking as input a scene-lib representation of a type augmented with
* additional information, stored in javac's format (e.g. as TypeMirrors or Elements). {@link
* ASceneWrapper} stores this additional information. This class walks the scene-lib representation
* structurally and outputs the stub file as a string, by combining the information scene-lib stores
* with the information gathered elsewhere.
*
*
The additional information is necessary because the scene-lib representation of a type does
* not have enough information to print full types.
*
*
This writer is used instead of {@code IndexFileWriter} if the {@code -Ainfer=stubs}
* command-line argument is present.
*/
public final class SceneToStubWriter {
/**
* The entry point to this class is {@link #write}.
*
*
This is a utility class with only static methods. It is not instantiable.
*/
private SceneToStubWriter() {
throw new BugInCF("Do not instantiate");
}
/**
* A pattern matching the name of an anonymous inner class, a local class, or a class nested
* within one of these types of classes. An anonymous inner class has a basename like Outer$1 and
* a local class has a basename like Outer$1Inner. See Java Language
* Specification, section 13.1.
*/
private static final Pattern anonymousInnerClassOrLocalClassPattern = Pattern.compile("\\$\\d+");
/** How far to indent when writing members of a stub file. */
private static final String INDENT = " ";
/**
* Writes the annotations in {@code scene} to {@code out} in stub file format.
*
* @param scene the scene to write out
* @param filename the name of the file to write (must end with .astub)
* @param checker the checker, for computing preconditions and postconditions
*/
public static void write(ASceneWrapper scene, String filename, BaseTypeChecker checker) {
writeImpl(scene, filename, checker);
}
/**
* Returns the part of a binary name that specifies the package.
*
* @param className the binary name of a class
* @return the part of the name referring to the package, or null if there is no package name
*/
@SuppressWarnings("signature") // a valid non-empty package name is a dot separated identifier
private static @Nullable @DotSeparatedIdentifiers String packagePart(
@BinaryName String className) {
int lastdot = className.lastIndexOf('.');
return (lastdot == -1) ? null : className.substring(0, lastdot);
}
/**
* Returns the part of a binary name that specifies the basename of the class.
*
* @param className a binary name
* @return the part of the name representing the class's name without its package
*/
@SuppressWarnings("signature:return") // A binary name without its package is still a binary name
private static @BinaryName String basenamePart(@BinaryName String className) {
int lastdot = className.lastIndexOf('.');
return className.substring(lastdot + 1);
}
/**
* Returns the String representation of an annotation in Java source format.
*
* @param a the annotation to print
* @return the formatted annotation
*/
public static String formatAnnotation(Annotation a) {
StringBuilder sb = new StringBuilder();
formatAnnotation(sb, a);
return sb.toString();
}
/**
* Formats an annotation in Java source format.
*
* @param sb where to format the annotation to
* @param a the annotation to print
*/
public static void formatAnnotation(StringBuilder sb, Annotation a) {
String fullAnnoName = a.def().name;
String simpleAnnoName = fullAnnoName.substring(fullAnnoName.lastIndexOf('.') + 1);
sb.append("@");
sb.append(simpleAnnoName);
if (a.fieldValues.isEmpty()) {
return;
} else {
sb.append("(");
if (a.fieldValues.size() == 1 && a.fieldValues.containsKey("value")) {
AnnotationFieldType aft = a.def().fieldTypes.get("value");
aft.format(sb, a.fieldValues.get("value"));
} else {
// This simulates: new StringJoiner(", ", "@" + simpleAnnoName + "(", ")")
for (Map.Entry f : a.fieldValues.entrySet()) {
AnnotationFieldType aft = a.def().fieldTypes.get(f.getKey());
sb.append(f.getKey());
sb.append("=");
aft.format(sb, f.getValue());
sb.append(", ");
}
sb.delete(sb.length() - 2, sb.length());
}
sb.append(")");
}
}
/**
* Returns all annotations in {@code annos} in a form suitable to be printed as Java source code.
*
* Each annotation is followed by a space, to separate it from following Java code.
*
* @param annos the annotations to format
* @return all annotations in {@code annos}, each followed by a space, in a form suitable to be
* printed as Java source code
*/
private static String formatAnnotations(Collection extends Annotation> annos) {
StringBuilder sb = new StringBuilder();
formatAnnotations(sb, annos);
return sb.toString();
}
/**
* Prints all annotations in {@code annos} to {@code sb} in a form suitable to be printed as Java
* source code.
*
*
Each annotation is followed by a space, to separate it from following Java code.
*
* @param sb where to write the formatted annotations
* @param annos the annotations to format
*/
private static void formatAnnotations(StringBuilder sb, Collection extends Annotation> annos) {
for (Annotation tla : annos) {
if (!isInternalJDKAnnotation(tla.def.name)) {
formatAnnotation(sb, tla);
sb.append(" ");
}
}
}
/**
* Formats the type of an array so that it is printable in Java source code, with the annotations
* from the scenelib representation added in appropriate places. The result includes a trailing
* space.
*
* @param sb where to format the array type to
* @param scenelibRep the array's scenelib type element
* @param javacRep the representation of the array's type used by javac
*/
private static void formatArrayType(
StringBuilder sb, ATypeElement scenelibRep, ArrayType javacRep) {
TypeMirror componentType = javacRep.getComponentType();
ATypeElement scenelibComponent = getNextArrayLevel(scenelibRep);
while (componentType.getKind() == TypeKind.ARRAY) {
componentType = ((ArrayType) componentType).getComponentType();
scenelibComponent = getNextArrayLevel(scenelibComponent);
}
formatType(sb, scenelibComponent, componentType);
formatArrayTypeImpl(sb, scenelibRep, javacRep);
}
/**
* Formats the type of an array to be printable in Java source code, with the annotations from the
* scenelib representation added. This method formats only the "array" parts of an array type; it
* does not format (or attempt to format) the ultimate component type (that is, the non-array part
* of the array type).
*
* @param sb where to format the array type to
* @param scenelibRep the scene-lib representation
* @param javacRep the javac representation of the array type
*/
private static void formatArrayTypeImpl(
StringBuilder sb, ATypeElement scenelibRep, ArrayType javacRep) {
TypeMirror javacComponent = javacRep.getComponentType();
ATypeElement scenelibComponent = getNextArrayLevel(scenelibRep);
List extends AnnotationMirror> explicitAnnos = javacRep.getAnnotationMirrors();
for (AnnotationMirror explicitAnno : explicitAnnos) {
sb.append(explicitAnno.toString());
sb.append(" ");
}
if (explicitAnnos.isEmpty() && scenelibRep != null) {
formatAnnotations(sb, scenelibRep.tlAnnotationsHere);
}
sb.append("[] ");
if (javacComponent.getKind() == TypeKind.ARRAY) {
formatArrayTypeImpl(sb, scenelibComponent, (ArrayType) javacComponent);
}
}
/** Static mutable variable to improve performance of getNextArrayLevel. */
private static List location;
/**
* Gets the outermost array level (or the component if not an array) from the given type element,
* or null if scene-lib is not storing any more information about this array (for example, when
* the component type is unannotated).
*
* @param e the array type element; can be null
* @return the next level of the array, if scene-lib stores information on it. null if the input
* is null or scene-lib is not storing more information.
*/
private static @Nullable ATypeElement getNextArrayLevel(@Nullable ATypeElement e) {
if (e == null) {
return null;
}
for (Map.Entry, ATypeElement> ite : e.innerTypes.entrySet()) {
location = ite.getKey();
if (location.contains(TypePathEntry.ARRAY_ELEMENT)) {
return ite.getValue();
}
}
return null;
}
/**
* Formats a single formal parameter declaration.
*
* @param param the AField that represents the parameter
* @param parameterName the name of the parameter to display in the stub file. Stub files
* disregard formal parameter names, so this is aesthetic in almost all cases. The exception
* is the receiver parameter, whose name must be "this".
* @param basename the type name to use for the receiver parameter. Only used when the previous
* argument is exactly the String "this".
* @return the formatted formal parameter, as if it were written in Java source code
*/
private static String formatParameter(AField param, String parameterName, String basename) {
StringBuilder sb = new StringBuilder();
formatParameter(sb, param, parameterName, basename);
return sb.toString();
}
/**
* Formats a single formal parameter declaration, as if it were written in Java source code.
*
* @param sb where to format the formal parameter to
* @param param the AField that represents the parameter
* @param parameterName the name of the parameter to display in the stub file. Stub files
* disregard formal parameter names, so this is aesthetic in almost all cases. The exception
* is the receiver parameter, whose name must be "this".
* @param basename the type name to use for the receiver parameter. Only used when the previous
* argument is exactly the String "this".
*/
private static void formatParameter(
StringBuilder sb, AField param, String parameterName, String basename) {
if (!param.tlAnnotationsHere.isEmpty()) {
for (Annotation declAnno : param.tlAnnotationsHere) {
formatAnnotation(sb, declAnno);
sb.append(" ");
}
sb.delete(sb.length() - 1, sb.length());
}
formatAFieldImpl(sb, param, parameterName, basename);
}
/**
* Formats a field declaration or formal parameter so that it can be printed in a stub.
*
* This method does not add a trailing semicolon or comma.
*
*
Usually, {@link #formatParameter(AField, String, String)} should be called to format method
* parameters, and {@link #printField(AField, String, PrintWriter, String)} should be called to
* print field declarations. Both use this method as their underlying implementation.
*
* @param aField the field declaration or formal parameter declaration to format; should not
* represent a local variable
* @param fieldName the name to use for the declaration in the stub file. This doesn't matter for
* parameters (except the "this" receiver parameter), but must be correct for fields.
* @param className the simple name of the enclosing class. This is only used for printing the
* type of an explicit receiver parameter (i.e., a parameter named "this").
* @return a String suitable to print in a stub file
*/
private static String formatAFieldImpl(AField aField, String fieldName, String className) {
StringBuilder sb = new StringBuilder();
formatAFieldImpl(sb, aField, fieldName, className);
return sb.toString();
}
/**
* Formats a field declaration or formal parameter so that it can be printed in a stub file.
*
*
This method does not add a trailing semicolon or comma.
*
*
Usually, {@link #formatParameter(AField, String, String)} should be called to format method
* parameters, and {@link #printField(AField, String, PrintWriter, String)} should be called to
* print field declarations. Both use this method as their underlying implementation.
*
* @param sb where to write the formatted declaration to
* @param aField the field declaration or formal parameter declaration to format; should not
* represent a local variable
* @param fieldName the name to use for the declaration in the stub file. This doesn't matter for
* parameters (except the "this" receiver parameter), but must be correct for fields.
* @param className the simple name of the enclosing class. This is only used for printing the
* type of an explicit receiver parameter (i.e., a parameter named "this").
*/
private static void formatAFieldImpl(
StringBuilder sb, AField aField, String fieldName, String className) {
if ("this".equals(fieldName)) {
formatType(sb, aField.type, null, className);
} else {
formatType(sb, aField.type, aField.getTypeMirror());
}
sb.append(fieldName);
}
/**
* Formats the given type for printing in Java source code.
*
* @param aType the scene-lib representation of the type, or null if only the unannotated type is
* to be printed
* @param javacType the javac representation of the type
* @return the type as it would appear in Java source code, followed by a trailing space
*/
private static String formatType(@Nullable ATypeElement aType, TypeMirror javacType) {
StringBuilder sb = new StringBuilder();
formatType(sb, aType, javacType);
return sb.toString();
}
/**
* Formats the given type as it would appear in Java source code, followed by a trailing space.
*
* @param sb where to format the type to
* @param aType the scene-lib representation of the type, or null if only the unannotated type is
* to be printed
* @param javacType the javac representation of the type
*/
private static void formatType(
StringBuilder sb, @Nullable ATypeElement aType, TypeMirror javacType) {
// TypeMirror#toString prints multiple annotations on a single type
// separated by commas rather than by whitespace, as is required in source code.
String basetypeToPrint = javacType.toString().replaceAll(",@", " @");
// We must not print annotations in the default package that conflict with
// imported annotation names.
for (AnnotationMirror anm : javacType.getAnnotationMirrors()) {
String annotationName = AnnotationUtils.annotationName(anm);
String simpleName = annotationName.substring(annotationName.lastIndexOf('.') + 1);
// This checks if it is in the default package.
if (simpleName.equals(annotationName)) {
// In that case, do not print any annotations with the type, to
// avoid needing to parse an annotation string to remove it.
// TypeMirror does not provide any methods to remove annotations.
// This code relies on unannotated Java types not including spaces.
basetypeToPrint = basetypeToPrint.substring(basetypeToPrint.lastIndexOf(' ') + 1);
}
}
formatType(sb, aType, javacType, basetypeToPrint);
}
/**
* Formats the given type as it would appear in Java source code, followed by a trailing space.
*
*
This overloaded version of this method exists only for receiver parameters, which are
* printed using the name of the class as {@code basetypeToPrint} instead of the javac type. The
* other version of this method should be preferred in every other case.
*
* @param sb where to formate the type to
* @param aType the scene-lib representation of the type, or null if only the unannotated type is
* to be printed
* @param javacType the javac representation of the type, or null if this is a receiver parameter
* @param basetypeToPrint the string representation of the type
*/
private static void formatType(
StringBuilder sb,
@Nullable ATypeElement aType,
@Nullable TypeMirror javacType,
String basetypeToPrint) {
// anonymous static classes shouldn't be printed with the "anonymous" tag that the AScene
// library uses
if (basetypeToPrint.startsWith(" 0) {
pos++;
if (basetypeToPrint.charAt(pos) == '<') {
openCount++;
}
if (basetypeToPrint.charAt(pos) == '>') {
openCount--;
}
}
basetypeToPrint =
basetypeToPrint.substring(0, basetypeToPrint.indexOf('<'))
+ basetypeToPrint.substring(pos + 1);
}
// An array is not a receiver, so using the javacType to check for arrays is safe.
if (javacType != null && javacType.getKind() == TypeKind.ARRAY) {
formatArrayType(sb, aType, (ArrayType) javacType);
return;
}
if (aType != null) {
formatAnnotations(sb, aType.tlAnnotationsHere);
}
sb.append(basetypeToPrint);
sb.append(" ");
}
/** Writes an import statement for each annotation used in an {@link AScene}. */
private static class ImportDefWriter extends DefCollector {
/** The writer onto which to write the import statements. */
private final PrintWriter printWriter;
/**
* Constructs a new ImportDefWriter, which will run on the given AScene when its {@code visit}
* method is called.
*
* @param scene the scene whose imported annotations should be printed
* @param printWriter the writer onto which to write the import statements
* @throws DefException if the DefCollector does not succeed
*/
ImportDefWriter(ASceneWrapper scene, PrintWriter printWriter) throws DefException {
super(scene.getAScene());
this.printWriter = printWriter;
}
/**
* Write an import statement for a given AnnotationDef. This is only called once per annotation
* used in the scene.
*
* @param d the annotation definition to print an import for
*/
@Override
protected void visitAnnotationDef(AnnotationDef d) {
if (!isInternalJDKAnnotation(d.name)) {
printWriter.println("import " + d.name + ";");
}
}
}
/**
* Return true if the given annotation is an internal JDK annotation, whose name includes '+'.
*
* @param annotationName the name of the annotation
* @return true iff this is an internal JDK annotation
*/
private static boolean isInternalJDKAnnotation(String annotationName) {
return annotationName.contains("+");
}
/**
* Print the hierarchy of outer classes up to and including the given class, and return the number
* of curly braces to close with. The classes are printed with appropriate opening curly braces,
* in standard Java style.
*
* In an AScene, an inner class name is a binary name like "Outer$Inner". In a stub file, inner
* classes must be nested, as in Java source code.
*
* @param basename the binary name of the class without the package part
* @param aClass the AClass for {@code basename}
* @param printWriter the writer where the class definition should be printed
* @param checker the type-checker whose annotations are being written
* @return the number of outer classes within which this class is nested
*/
private static int printClassDefinitions(
String basename, AClass aClass, PrintWriter printWriter, BaseTypeChecker checker) {
String[] classNames = basename.split("\\$");
TypeElement innermostTypeElt = aClass.getTypeElement();
if (innermostTypeElt == null) {
throw new BugInCF("typeElement was unexpectedly null in this aClass: " + aClass);
}
TypeElement[] typeElements = getTypeElementsForClasses(innermostTypeElt, classNames);
for (int i = 0; i < classNames.length; i++) {
String nameToPrint = classNames[i];
if (i == classNames.length - 1) {
printWriter.print(indents(i));
printWriter.println("@AnnotatedFor(\"" + checker.getClass().getCanonicalName() + "\")");
}
printWriter.print(indents(i));
if (i == classNames.length - 1) {
// Only print class annotations on the innermost class, which corresponds to aClass.
// If there should be class annotations on another class, it will have its own stub
// file, which will eventually be merged with this one.
printWriter.print(formatAnnotations(aClass.getAnnotations()));
}
if (aClass.isAnnotation(nameToPrint)) {
printWriter.print("@interface ");
} else if (aClass.isEnum(nameToPrint)) {
printWriter.print("enum ");
} else if (aClass.isInterface(nameToPrint)) {
printWriter.print("interface ");
} else if (aClass.isRecord(nameToPrint)) {
printWriter.print("record ");
} else {
printWriter.print("class ");
}
printWriter.print(nameToPrint);
printTypeParameters(typeElements[i], printWriter);
printWriter.println(" {");
if (aClass.isEnum(nameToPrint) && i != classNames.length - 1) {
// Print a blank set of enum constants if this is an outer enum.
printWriter.println(indents(i + 1) + "/* omitted enum constants */ ;");
}
printWriter.println();
}
return classNames.length;
}
/**
* Constructs an array of TypeElements corresponding to the list of classes.
*
* @param innermostTypeElt the innermost type element: either an inner class or an outer class
* without any inner classes that should be printed
* @param classNames the names of the containing classes, from outer to inner
* @return an array of TypeElements whose entry at a given index represents the type named at that
* index in {@code classNames}
*/
private static TypeElement @SameLen("#2") [] getTypeElementsForClasses(
TypeElement innermostTypeElt, String @MinLen(1) [] classNames) {
TypeElement[] result = new TypeElement[classNames.length];
result[classNames.length - 1] = innermostTypeElt;
Element elt = innermostTypeElt;
for (int i = classNames.length - 2; i >= 0; i--) {
elt = elt.getEnclosingElement();
result[i] = (TypeElement) elt;
}
return result;
}
/**
* Prints all the fields of a given class.
*
* @param aClass the class whose fields should be printed
* @param printWriter the writer on which to print the fields
* @param indentLevel the indent string
*/
private static void printFields(AClass aClass, PrintWriter printWriter, String indentLevel) {
if (aClass.getFields().isEmpty()) {
return;
}
printWriter.println(indentLevel + "// fields:");
printWriter.println();
for (Map.Entry fieldEntry : aClass.getFields().entrySet()) {
String fieldName = fieldEntry.getKey();
AField aField = fieldEntry.getValue();
printField(aField, fieldName, printWriter, indentLevel);
}
}
/**
* Prints a field declaration, including a trailing semicolon and a newline.
*
* @param aField the field declaration
* @param fieldName the name of the field
* @param printWriter the writer on which to print
* @param indentLevel the indent string
*/
private static void printField(
AField aField, String fieldName, PrintWriter printWriter, String indentLevel) {
if (aField.getTypeMirror() == null) {
// aField has no type mirror, so there are no inferred annotations and the field need
// not be printed.
return;
}
for (Annotation declAnno : aField.tlAnnotationsHere) {
printWriter.print(indentLevel);
printWriter.println(formatAnnotation(declAnno));
}
printWriter.print(indentLevel);
printWriter.print(formatAFieldImpl(aField, fieldName, /*enclosing class=*/ null));
printWriter.println(";");
printWriter.println();
}
/**
* Prints a method declaration in stub file format (i.e., without a method body).
*
* @param aMethod the method to print
* @param simplename the simple name of the enclosing class, for receiver parameters and
* constructor names
* @param printWriter where to print the method signature
* @param atf the type factory, for computing preconditions and postconditions
* @param indentLevel the indent string
*/
private static void printMethodDeclaration(
AMethod aMethod,
String simplename,
PrintWriter printWriter,
String indentLevel,
GenericAnnotatedTypeFactory, ?, ?, ?> atf) {
if (aMethod.getTypeParameters() == null) {
// aMethod.setFieldsFromMethodElement has not been called
return;
}
for (Annotation declAnno : aMethod.tlAnnotationsHere) {
printWriter.print(indentLevel);
printWriter.println(formatAnnotation(declAnno));
}
for (AnnotationMirror contractAnno : atf.getContractAnnotations(aMethod)) {
printWriter.print(indentLevel);
printWriter.println(contractAnno);
}
printWriter.print(indentLevel);
printTypeParameters(aMethod.getTypeParameters(), printWriter);
String methodName = aMethod.getMethodName();
// Use Java syntax for constructors.
if ("".equals(methodName)) {
// Set methodName, but don't output a return type.
methodName = simplename;
} else {
printWriter.print(formatType(aMethod.returnType, aMethod.getReturnTypeMirror()));
}
printWriter.print(methodName);
printWriter.print("(");
StringJoiner parameters = new StringJoiner(", ");
if (!aMethod.receiver.type.tlAnnotationsHere.isEmpty()) {
// Only output the receiver if it has an annotation.
parameters.add(formatParameter(aMethod.receiver, "this", simplename));
}
for (Integer index : aMethod.getParameters().keySet()) {
AField param = aMethod.getParameters().get(index);
parameters.add(formatParameter(param, param.getName(), simplename));
}
printWriter.print(parameters.toString());
printWriter.println(");");
printWriter.println();
}
/**
* The implementation of {@link #write}. Prints imports, classes, method signatures, and fields in
* stub file format, all with appropriate annotations.
*
* @param scene the scene to write
* @param filename the name of the file to write (must end in .astub)
* @param checker the checker, for computing preconditions
*/
private static void writeImpl(ASceneWrapper scene, String filename, BaseTypeChecker checker) {
// Sort by package name first so that output is deterministic and default package
// comes first; within package sort by class name.
@SuppressWarnings("signature") // scene-lib bytecode lacks signature annotations
List<@BinaryName String> classes = new ArrayList<>(scene.getAScene().getClasses().keySet());
Collections.sort(
classes,
(o1, o2) ->
ComparisonChain.start()
.compare(
packagePart(o1),
packagePart(o2),
Comparator.nullsFirst(Comparator.naturalOrder()))
.compare(basenamePart(o1), basenamePart(o2))
.result());
boolean anyClassPrintable = false;
// The writer is not initialized until it is certain that at
// least one class can be written, to avoid empty stub files.
// An alternate approach would be to delete the file after it is closed, if the file is
// empty.
// It's not worth rewriting this code, since .stub files are obsolescent.
FileWriter fileWriter = null;
PrintWriter printWriter = null;
try {
// For each class
for (String clazz : classes) {
if (isPrintable(clazz, scene.getAScene().getClasses().get(clazz))) {
if (!anyClassPrintable) {
try {
if (fileWriter != null || printWriter != null) {
throw new Error("This can't happen");
}
fileWriter = new FileWriter(filename);
printWriter = new PrintWriter(fileWriter);
} catch (IOException e) {
throw new BugInCF("error writing file during WPI: " + filename);
}
// Write out all imports
ImportDefWriter importDefWriter;
try {
importDefWriter = new ImportDefWriter(scene, printWriter);
} catch (DefException e) {
throw new BugInCF(e);
}
importDefWriter.visit();
printWriter.println("import org.checkerframework.framework.qual.AnnotatedFor;");
printWriter.println();
anyClassPrintable = true;
}
printClass(clazz, scene.getAScene().getClasses().get(clazz), checker, printWriter);
}
}
} finally {
if (printWriter != null) {
printWriter.close(); // does not throw IOException
}
try {
if (fileWriter != null) {
fileWriter.close();
}
} catch (IOException e) {
// Nothing to do since exceptions thrown from a finally block have no effect.
}
}
}
/**
* Returns true if the class is printable in a stub file. A printable class is a class or enum
* (not a package or module) and is not anonymous.
*
* @param classname the class name
* @param aClass the representation of the class
* @return whether the class is printable, by the definition above
*/
private static boolean isPrintable(@BinaryName String classname, AClass aClass) {
String basename = basenamePart(classname);
if ("package-info".equals(basename) || "module-info".equals(basename)) {
return false;
}
// Do not attempt to print stubs for anonymous inner classes, local classes, or their inner
// classes, because the stub parser cannot read them.
if (anonymousInnerClassOrLocalClassPattern.matcher(basename).find()) {
return false;
}
if (aClass.getTypeElement() == null) {
throw new BugInCF(
"Tried printing an unprintable class to a stub file during WPI: " + aClass.className);
}
return true;
}
/**
* Print the class body, or nothing if this is an anonymous inner class. Call {@link
* #isPrintable(String, AClass)} and check that it returns true before calling this method.
*
* @param classname the class name
* @param aClass the representation of the class
* @param checker the checker, for computing preconditions
* @param printWriter the writer on which to print
*/
private static void printClass(
@BinaryName String classname,
AClass aClass,
BaseTypeChecker checker,
PrintWriter printWriter) {
String basename = basenamePart(classname);
String innermostClassname =
basename.contains("$") ? basename.substring(basename.lastIndexOf('$') + 1) : basename;
String pkg = packagePart(classname);
if (pkg != null) {
printWriter.println("package " + pkg + ";");
}
int curlyCount = printClassDefinitions(basename, aClass, printWriter, checker);
String indentLevel = indents(curlyCount);
List enumConstants = aClass.getEnumConstants();
if (enumConstants != null) {
StringJoiner sj = new StringJoiner(", ");
for (VariableElement enumConstant : enumConstants) {
sj.add(enumConstant.getSimpleName());
}
printWriter.println(indentLevel + "// enum constants:");
printWriter.println();
printWriter.println(indentLevel + sj.toString() + ";");
printWriter.println();
}
printFields(aClass, printWriter, indentLevel);
if (!aClass.getMethods().isEmpty()) {
// print method signatures
printWriter.println(indentLevel + "// methods:");
printWriter.println();
for (Map.Entry methodEntry : aClass.getMethods().entrySet()) {
printMethodDeclaration(
methodEntry.getValue(),
innermostClassname,
printWriter,
indentLevel,
checker.getTypeFactory());
}
}
for (int i = 0; i < curlyCount; i++) {
printWriter.println(indents(curlyCount - i - 1) + "}");
}
}
/**
* Return a string containing n indents.
*
* @param n the number of indents
* @return a string containing that many indents
*/
private static String indents(int n) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(INDENT);
}
return sb.toString();
}
/**
* Prints the type parameters of the given class, enclosed in {@code <...>}.
*
* @param type the TypeElement representing the class whose type parameters should be printed
* @param printWriter where to print the type parameters
*/
private static void printTypeParameters(TypeElement type, PrintWriter printWriter) {
List extends TypeParameterElement> typeParameters = type.getTypeParameters();
printTypeParameters(typeParameters, printWriter);
}
/**
* Prints the given type parameters.
*
* @param typeParameters the type element to print
* @param printWriter where to print the type parameters
*/
private static void printTypeParameters(
List extends TypeParameterElement> typeParameters, PrintWriter printWriter) {
if (typeParameters.isEmpty()) {
return;
}
StringJoiner sj = new StringJoiner(", ", "<", ">");
for (TypeParameterElement t : typeParameters) {
sj.add(t.getSimpleName().toString());
}
printWriter.print(sj.toString());
}
}