| 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.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; |
| 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.AnnotationDef; |
| import scenelib.annotations.el.DefCollector; |
| import scenelib.annotations.el.DefException; |
| import scenelib.annotations.el.TypePathEntry; |
| import scenelib.annotations.field.AnnotationFieldType; |
| |
| // 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. |
| * |
| * <p>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. |
| * |
| * <p>The additional information is necessary because the scene-lib representation of a type does |
| * not have enough information to print full types. |
| * |
| * <p>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}. |
| * |
| * <p>This is a utility class with only static methods. It is not instantiable. |
| */ |
| private SceneToStubWriter() { |
| throw new Error("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 <a |
| * href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-13.html#jls-13.1">Java Language |
| * Specification, section 13.1</a>. |
| */ |
| 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) { |
| String fullAnnoName = a.def().name; |
| String simpleAnnoName = fullAnnoName.substring(fullAnnoName.lastIndexOf('.') + 1); |
| if (a.fieldValues.isEmpty()) { |
| return "@" + simpleAnnoName; |
| } |
| StringJoiner sj = new StringJoiner(", ", "@" + simpleAnnoName + "(", ")"); |
| if (a.fieldValues.size() == 1 && a.fieldValues.containsKey("value")) { |
| AnnotationFieldType aft = a.def().fieldTypes.get("value"); |
| sj.add(aft.format(a.fieldValues.get("value"))); |
| } else { |
| for (Map.Entry<String, Object> f : a.fieldValues.entrySet()) { |
| AnnotationFieldType aft = a.def().fieldTypes.get(f.getKey()); |
| sj.add(f.getKey() + "=" + aft.format(f.getValue())); |
| } |
| } |
| return sj.toString(); |
| } |
| |
| /** |
| * Returns all annotations in {@code annos} in a form suitable to be printed as Java source code. |
| * |
| * <p>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(); |
| for (Annotation tla : annos) { |
| if (!isInternalJDKAnnotation(tla.def.name)) { |
| sb.append(formatAnnotation(tla)); |
| sb.append(" "); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * 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. |
| * |
| * @param scenelibRep the array's scenelib type element |
| * @param javacRep the representation of the array's type used by javac |
| * @return the type formatted to be written to Java source code, followed by a space character |
| */ |
| private static String formatArrayType(ATypeElement scenelibRep, ArrayType javacRep) { |
| TypeMirror componentType = javacRep.getComponentType(); |
| ATypeElement scenelibComponent = getNextArrayLevel(scenelibRep); |
| while (componentType.getKind() == TypeKind.ARRAY) { |
| componentType = ((ArrayType) componentType).getComponentType(); |
| scenelibComponent = getNextArrayLevel(scenelibComponent); |
| } |
| return formatType(scenelibComponent, componentType) |
| + formatArrayTypeImpl(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 scenelibRep the scene-lib representation |
| * @param javacRep the javac representation of the array type |
| * @return the type formatted to be written to Java source code, followed by a space character |
| */ |
| private static String formatArrayTypeImpl(ATypeElement scenelibRep, ArrayType javacRep) { |
| TypeMirror javacComponent = javacRep.getComponentType(); |
| ATypeElement scenelibComponent = getNextArrayLevel(scenelibRep); |
| String result = ""; |
| List<? extends AnnotationMirror> explicitAnnos = javacRep.getAnnotationMirrors(); |
| for (AnnotationMirror explicitAnno : explicitAnnos) { |
| result += explicitAnno.toString(); |
| result += " "; |
| } |
| if (result.isEmpty() && scenelibRep != null) { |
| result += formatAnnotations(scenelibRep.tlAnnotationsHere); |
| } |
| result += "[] "; |
| if (javacComponent.getKind() == TypeKind.ARRAY) { |
| return result + formatArrayTypeImpl(scenelibComponent, (ArrayType) javacComponent); |
| } else { |
| return result; |
| } |
| } |
| |
| /** Static variable to improve performance of getNextArrayLevel. */ |
| private static List<TypePathEntry> 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<List<TypePathEntry>, 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) { |
| StringJoiner result = new StringJoiner(" "); |
| for (Annotation declAnno : param.tlAnnotationsHere) { |
| result.add(formatAnnotation(declAnno)); |
| } |
| result.add(formatAFieldImpl(param, parameterName, basename)); |
| return result.toString(); |
| } |
| |
| /** |
| * Formats a field declaration or formal parameter so that it can be printed in a stub. |
| * |
| * <p>This method does not add a trailing semicolon or comma. |
| * |
| * <p>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) { |
| if ("this".equals(fieldName)) { |
| return formatType(aField.type, null, className) + fieldName; |
| } else { |
| return formatType(aField.type, aField.getTypeMirror()) + 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(final @Nullable ATypeElement aType, final 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); |
| } |
| } |
| return formatType(aType, javacType, basetypeToPrint); |
| } |
| |
| /** |
| * Formats the given type for printing in Java source code. This separate 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 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 |
| * @return the type as it would appear in Java source code, followed by a trailing space |
| */ |
| private static String formatType( |
| final @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("<anonymous ")) { |
| basetypeToPrint = |
| basetypeToPrint.substring("<anonymous ".length(), basetypeToPrint.length() - 1); |
| } |
| |
| // fields don't need their generic types, and sometimes they are wrong. Just don't print them. |
| while (basetypeToPrint.contains("<")) { |
| basetypeToPrint = |
| basetypeToPrint.substring(0, basetypeToPrint.indexOf('<')) |
| + basetypeToPrint.substring(basetypeToPrint.lastIndexOf('>') + 1); |
| } |
| |
| // An array is not a receiver, so using the javacType to check for arrays is safe. |
| if (javacType != null && javacType.getKind() == TypeKind.ARRAY) { |
| return formatArrayType(aType, (ArrayType) javacType); |
| } |
| |
| if (aType == null) { |
| return basetypeToPrint + " "; |
| } else { |
| return formatAnnotations(aType.tlAnnotationsHere) + basetypeToPrint + " "; |
| } |
| } |
| |
| /** 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. |
| * |
| * <p>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 (aClass.isEnum(nameToPrint)) { |
| printWriter.print("enum "); |
| } else { |
| printWriter.print("class "); |
| } |
| 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())); |
| } |
| 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<String, AField> 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 ("<init>".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, |
| new Comparator<@BinaryName String>() { |
| @Override |
| public int compare(@BinaryName String o1, @BinaryName String o2) { |
| return 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. |
| PrintWriter printWriter = null; |
| |
| // For each class |
| for (String clazz : classes) { |
| if (isPrintable(clazz, scene.getAScene().getClasses().get(clazz))) { |
| if (!anyClassPrintable) { |
| try { |
| printWriter = new PrintWriter(new FileWriter(filename)); |
| } 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); |
| } |
| } |
| if (printWriter != null) { |
| printWriter.flush(); |
| } |
| } |
| |
| /** |
| * 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<VariableElement> 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<String, AMethod> 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()); |
| } |
| } |