| package org.checkerframework.framework.stub; |
| |
| import com.github.javaparser.ast.CompilationUnit; |
| import com.github.javaparser.ast.Node; |
| import com.github.javaparser.ast.StubUnit; |
| import com.github.javaparser.ast.body.BodyDeclaration; |
| import com.github.javaparser.ast.body.ConstructorDeclaration; |
| import com.github.javaparser.ast.body.FieldDeclaration; |
| import com.github.javaparser.ast.body.MethodDeclaration; |
| import com.github.javaparser.ast.body.Parameter; |
| import com.github.javaparser.ast.body.TypeDeclaration; |
| import com.github.javaparser.ast.body.VariableDeclarator; |
| import com.github.javaparser.ast.type.ClassOrInterfaceType; |
| import com.github.javaparser.ast.type.PrimitiveType; |
| import com.github.javaparser.ast.type.VoidType; |
| import com.github.javaparser.ast.type.WildcardType; |
| import com.github.javaparser.ast.visitor.SimpleVoidVisitor; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.Enumeration; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import javax.lang.model.element.Element; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.element.TypeElement; |
| import javax.lang.model.element.VariableElement; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| import org.checkerframework.checker.signature.qual.FullyQualifiedName; |
| import org.checkerframework.javacutil.BugInCF; |
| import org.checkerframework.javacutil.Pair; |
| |
| /** Utility class for annotation files (stub files and ajava files). */ |
| public class AnnotationFileUtil { |
| /** |
| * The types of files that can contain annotations. Also indicates the file's source, such as from |
| * the JDK, built in, or from the command line. |
| * |
| * <p>Stub files have extension ".astub". Ajava files have extension ".ajava". |
| */ |
| public enum AnnotationFileType { |
| /** Stub file in the annotated JDK. */ |
| JDK_STUB, |
| /** Stub file built into a checker. */ |
| BUILTIN_STUB, |
| /** Stub file provided on command line. */ |
| COMMAND_LINE_STUB, |
| /** Ajava file being parsed as if it is a stub file. */ |
| AJAVA_AS_STUB, |
| /** Ajava file provided on command line. */ |
| AJAVA; |
| |
| /** |
| * Returns true if this represents a stub file. |
| * |
| * @return true if this represents a stub file |
| */ |
| public boolean isStub() { |
| switch (this) { |
| case JDK_STUB: |
| case BUILTIN_STUB: |
| case COMMAND_LINE_STUB: |
| case AJAVA_AS_STUB: |
| return true; |
| case AJAVA: |
| return false; |
| default: |
| throw new Error("unhandled case " + this); |
| } |
| } |
| |
| /** |
| * Returns true if this annotation file is built-in (not provided on the command line). |
| * |
| * @return true if this annotation file is built-in (not provided on the command line) |
| */ |
| public boolean isBuiltIn() { |
| switch (this) { |
| case JDK_STUB: |
| case BUILTIN_STUB: |
| return true; |
| case COMMAND_LINE_STUB: |
| case AJAVA_AS_STUB: |
| case AJAVA: |
| return false; |
| default: |
| throw new Error("unhandled case " + this); |
| } |
| } |
| |
| /** |
| * Returns true if this annotation file was provided on the command line (not built-in). |
| * |
| * @return true if this annotation file was provided on the command line (not built-in) |
| */ |
| public boolean isCommandLine() { |
| switch (this) { |
| case JDK_STUB: |
| case BUILTIN_STUB: |
| return false; |
| case COMMAND_LINE_STUB: |
| case AJAVA_AS_STUB: |
| case AJAVA: |
| return true; |
| default: |
| throw new Error("unhandled case " + this); |
| } |
| } |
| } |
| |
| /** |
| * Finds the type declaration with the given class name in a StubUnit. |
| * |
| * @param className fully qualified name of the type declaration to find |
| * @param indexFile StubUnit to search |
| * @return the declaration in {@code indexFile} with {@code className} if it exists, null |
| * otherwise. |
| */ |
| /*package-scope*/ static TypeDeclaration<?> findDeclaration( |
| String className, StubUnit indexFile) { |
| int indexOfDot = className.lastIndexOf('.'); |
| |
| if (indexOfDot == -1) { |
| // classes not within a package needs to be the first in the index file |
| assert !indexFile.getCompilationUnits().isEmpty(); |
| assert indexFile.getCompilationUnits().get(0).getPackageDeclaration() == null; |
| return findDeclaration(className, indexFile.getCompilationUnits().get(0)); |
| } |
| |
| final String packageName = className.substring(0, indexOfDot); |
| final String simpleName = className.substring(indexOfDot + 1); |
| |
| for (CompilationUnit cu : indexFile.getCompilationUnits()) { |
| if (cu.getPackageDeclaration().isPresent() |
| && cu.getPackageDeclaration().get().getNameAsString().equals(packageName)) { |
| TypeDeclaration<?> type = findDeclaration(simpleName, cu); |
| if (type != null) { |
| return type; |
| } |
| } |
| } |
| |
| // Couldn't find it |
| return null; |
| } |
| |
| /*package-scope*/ static TypeDeclaration<?> findDeclaration( |
| TypeElement type, StubUnit indexFile) { |
| return findDeclaration(type.getQualifiedName().toString(), indexFile); |
| } |
| |
| /*package-scope*/ static FieldDeclaration findDeclaration( |
| VariableElement field, StubUnit indexFile) { |
| TypeDeclaration<?> type = findDeclaration((TypeElement) field.getEnclosingElement(), indexFile); |
| if (type == null) { |
| return null; |
| } |
| |
| for (BodyDeclaration<?> member : type.getMembers()) { |
| if (!(member instanceof FieldDeclaration)) { |
| continue; |
| } |
| FieldDeclaration decl = (FieldDeclaration) member; |
| for (VariableDeclarator var : decl.getVariables()) { |
| if (toString(var).equals(field.getSimpleName().toString())) { |
| return decl; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /*package-scope*/ static BodyDeclaration<?> findDeclaration( |
| ExecutableElement method, StubUnit indexFile) { |
| TypeDeclaration<?> type = |
| findDeclaration((TypeElement) method.getEnclosingElement(), indexFile); |
| if (type == null) { |
| return null; |
| } |
| |
| String methodRep = toString(method); |
| |
| for (BodyDeclaration<?> member : type.getMembers()) { |
| if (member instanceof MethodDeclaration) { |
| if (toString((MethodDeclaration) member).equals(methodRep)) { |
| return member; |
| } |
| } else if (member instanceof ConstructorDeclaration) { |
| if (toString((ConstructorDeclaration) member).equals(methodRep)) { |
| return member; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /*package-scope*/ static TypeDeclaration<?> findDeclaration( |
| String simpleName, CompilationUnit cu) { |
| for (TypeDeclaration<?> type : cu.getTypes()) { |
| if (simpleName.equals(type.getNameAsString())) { |
| return type; |
| } |
| } |
| // Couldn't find it |
| return null; |
| } |
| |
| /*package-scope*/ static String toString(MethodDeclaration method) { |
| return ElementPrinter.toString(method); |
| } |
| |
| /*package-scope*/ static String toString(ConstructorDeclaration constructor) { |
| return ElementPrinter.toString(constructor); |
| } |
| |
| /*package-scope*/ static String toString(VariableDeclarator field) { |
| return field.getNameAsString(); |
| } |
| |
| /*package-scope*/ static String toString(FieldDeclaration field) { |
| assert field.getVariables().size() == 1; |
| return toString(field.getVariables().get(0)); |
| } |
| |
| /*package-scope*/ static String toString(VariableElement element) { |
| assert element.getKind().isField(); |
| return element.getSimpleName().toString(); |
| } |
| |
| /*package-scope*/ static String toString(Element element) { |
| if (element instanceof ExecutableElement) { |
| return toString((ExecutableElement) element); |
| } else if (element instanceof VariableElement) { |
| return toString((VariableElement) element); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Split a name (which comes from an import statement) into the part before the last period and |
| * the part after the last period. |
| * |
| * @param imported the name to split |
| * @return a pair of the type name and the field name |
| */ |
| @SuppressWarnings("signature") // string parsing |
| public static Pair<@FullyQualifiedName String, String> partitionQualifiedName(String imported) { |
| @FullyQualifiedName String typeName = imported.substring(0, imported.lastIndexOf(".")); |
| String name = imported.substring(imported.lastIndexOf(".") + 1); |
| Pair<String, String> typeParts = Pair.of(typeName, name); |
| return typeParts; |
| } |
| |
| private static final class ElementPrinter extends SimpleVoidVisitor<Void> { |
| public static String toString(Node n) { |
| ElementPrinter printer = new ElementPrinter(); |
| n.accept(printer, null); |
| return printer.getOutput(); |
| } |
| |
| private final StringBuilder sb = new StringBuilder(); |
| |
| public String getOutput() { |
| return sb.toString(); |
| } |
| |
| @Override |
| public void visit(ConstructorDeclaration n, Void arg) { |
| sb.append("<init>"); |
| |
| sb.append("("); |
| if (n.getParameters() != null) { |
| for (Iterator<Parameter> i = n.getParameters().iterator(); i.hasNext(); ) { |
| Parameter p = i.next(); |
| p.accept(this, arg); |
| |
| if (i.hasNext()) { |
| sb.append(","); |
| } |
| } |
| } |
| sb.append(")"); |
| } |
| |
| @Override |
| public void visit(MethodDeclaration n, Void arg) { |
| sb.append(n.getName()); |
| |
| sb.append("("); |
| if (n.getParameters() != null) { |
| for (Iterator<Parameter> i = n.getParameters().iterator(); i.hasNext(); ) { |
| Parameter p = i.next(); |
| p.accept(this, arg); |
| |
| if (i.hasNext()) { |
| sb.append(","); |
| } |
| } |
| } |
| sb.append(")"); |
| } |
| |
| @Override |
| public void visit(Parameter n, Void arg) { |
| n.getType().accept(this, arg); |
| if (n.isVarArgs()) { |
| sb.append("[]"); |
| } |
| } |
| |
| // Types |
| @Override |
| public void visit(ClassOrInterfaceType n, Void arg) { |
| sb.append(n.getName()); |
| } |
| |
| @Override |
| public void visit(PrimitiveType n, Void arg) { |
| switch (n.getType()) { |
| case BOOLEAN: |
| sb.append("boolean"); |
| break; |
| case BYTE: |
| sb.append("byte"); |
| break; |
| case CHAR: |
| sb.append("char"); |
| break; |
| case DOUBLE: |
| sb.append("double"); |
| break; |
| case FLOAT: |
| sb.append("float"); |
| break; |
| case INT: |
| sb.append("int"); |
| break; |
| case LONG: |
| sb.append("long"); |
| break; |
| case SHORT: |
| sb.append("short"); |
| break; |
| default: |
| throw new BugInCF("AnnotationFileUtil: unknown type: " + n.getType()); |
| } |
| } |
| |
| @Override |
| public void visit(com.github.javaparser.ast.type.ArrayType n, Void arg) { |
| n.getComponentType().accept(this, arg); |
| sb.append("[]"); |
| } |
| |
| @Override |
| public void visit(VoidType n, Void arg) { |
| sb.append("void"); |
| } |
| |
| @Override |
| public void visit(WildcardType n, Void arg) { |
| // We don't write type arguments |
| // TODO: Why? |
| throw new BugInCF("AnnotationFileUtil: don't print type args"); |
| } |
| } |
| |
| /** |
| * Return annotation files found at a given file system location (does not look on classpath). |
| * |
| * @param location an annotation file (stub file or ajava file), a jarfile, or a directory. Look |
| * for it as an absolute file and relative to the current directory. |
| * @param fileType file type of files to collect |
| * @return annotation files with the given file type found in the file system (does not look on |
| * classpath). Returns null if the file system location does not exist; the caller may wish to |
| * issue a warning in that case. |
| */ |
| public static @Nullable List<AnnotationFileResource> allAnnotationFiles( |
| String location, AnnotationFileType fileType) { |
| File file = new File(location); |
| if (file.exists()) { |
| List<AnnotationFileResource> resources = new ArrayList<>(); |
| addAnnotationFilesToList(file, resources, fileType); |
| return resources; |
| } |
| |
| // The file doesn't exist. Maybe it is relative to the current working directory, so try that. |
| String workingDir = System.getProperty("user.dir") + System.getProperty("file.separator"); |
| file = new File(workingDir + location); |
| if (file.exists()) { |
| List<AnnotationFileResource> resources = new ArrayList<>(); |
| addAnnotationFilesToList(file, resources, fileType); |
| return resources; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns true if the given file is an annotation file of the given type. |
| * |
| * @param f the file to check |
| * @param fileType the type of file to check against |
| * @return true if {@code f} is a file with file type matching {@code fileType}, false otherwise |
| */ |
| private static boolean isAnnotationFile(File f, AnnotationFileType fileType) { |
| return f.isFile() && isAnnotationFile(f.getName(), fileType); |
| } |
| |
| /** |
| * Returns true if the given file is an annotation file of the given kind. |
| * |
| * @param path a file |
| * @param fileType the type of file to check against |
| * @return true if {@code path} represents a file with file type matching {@code fileType}, false |
| * otherwise |
| */ |
| private static boolean isAnnotationFile(String path, AnnotationFileType fileType) { |
| return path.endsWith(fileType.isStub() ? ".astub" : ".ajava"); |
| } |
| |
| private static boolean isJar(File f) { |
| return f.isFile() && f.getName().endsWith(".jar"); |
| } |
| |
| /** |
| * Side-effects {@code resources} by adding annotation files of the given file type to it. |
| * |
| * @param location an annotation file (a stub file or ajava file), a jarfile, or a directory. If a |
| * stub file or ajava file, add it to the {@code resources} list. If a jarfile, use all |
| * annotation files (of type {@code fileType}) contained in it. If a directory, recurse on all |
| * files contained in it. |
| * @param resources the list to add the found files to |
| * @param fileType type of annotation files to add |
| */ |
| @SuppressWarnings("JdkObsolete") // JarFile.entries() |
| private static void addAnnotationFilesToList( |
| File location, List<AnnotationFileResource> resources, AnnotationFileType fileType) { |
| if (isAnnotationFile(location, fileType)) { |
| resources.add(new FileAnnotationFileResource(location)); |
| } else if (isJar(location)) { |
| JarFile file; |
| try { |
| file = new JarFile(location); |
| } catch (IOException e) { |
| System.err.println("AnnotationFileUtil: could not process JAR file: " + location); |
| return; |
| } |
| Enumeration<JarEntry> entries = file.entries(); |
| while (entries.hasMoreElements()) { |
| JarEntry entry = entries.nextElement(); |
| if (isAnnotationFile(entry.getName(), fileType)) { |
| resources.add(new JarEntryAnnotationFileResource(file, entry)); |
| } |
| } |
| } else if (location.isDirectory()) { |
| File[] directoryContents = location.listFiles(); |
| Arrays.sort( |
| directoryContents, |
| new Comparator<File>() { |
| @Override |
| public int compare(File o1, File o2) { |
| return o1.getName().compareTo(o2.getName()); |
| } |
| }); |
| for (File enclosed : directoryContents) { |
| addAnnotationFilesToList(enclosed, resources, fileType); |
| } |
| } |
| } |
| } |