org.checkerframework.framework.stub.AnnotationFileUtil Maven / Gradle / Ivy
Show all versions of checker Show documentation
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 org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.plumelib.util.IPair;
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.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Types;
/** 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.
*
* 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 BugInCF("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 BugInCF("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 BugInCF("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 a StubUnit to search
* @return the declaration in {@code indexFile} with {@code className} if it exists, null
* otherwise.
*/
/*package-private*/ static @Nullable 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
CompilationUnit cu = indexFile.getCompilationUnits().get(0);
assert !cu.getPackageDeclaration().isPresent();
return findDeclaration(className, cu);
}
String packageName = className.substring(0, indexOfDot);
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-private*/ static TypeDeclaration> findDeclaration(
TypeElement type, StubUnit indexFile) {
return findDeclaration(type.getQualifiedName().toString(), indexFile);
}
/*package-private*/ static @Nullable 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-private*/ static @Nullable 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-private*/ static @Nullable 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-private*/ static String toString(MethodDeclaration method) {
return ElementPrinter.toString(method);
}
/*package-private*/ static String toString(ConstructorDeclaration constructor) {
return ElementPrinter.toString(constructor);
}
/*package-private*/ static String toString(VariableDeclarator field) {
return field.getNameAsString();
}
/*package-private*/ static String toString(FieldDeclaration field) {
assert field.getVariables().size() == 1;
return toString(field.getVariables().get(0));
}
/*package-private*/ static String toString(VariableElement element) {
assert element.getKind().isField();
return element.getSimpleName().toString();
}
/*package-private*/ static @Nullable 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 IPair<@FullyQualifiedName String, String> partitionQualifiedName(
String imported) {
@FullyQualifiedName String typeName = imported.substring(0, imported.lastIndexOf("."));
String name = imported.substring(imported.lastIndexOf(".") + 1);
IPair typeParts = IPair.of(typeName, name);
return typeParts;
}
private static final class ElementPrinter extends SimpleVoidVisitor {
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("");
sb.append("(");
if (n.getParameters() != null) {
for (Iterator 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 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 allAnnotationFiles(
String location, AnnotationFileType fileType) {
File file = new File(location);
if (file.exists()) {
List 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.
file = new File(System.getProperty("user.dir"), location);
if (file.exists()) {
List 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()
"nullness:argument", // inference failed in Arrays.sort
"builder:required.method.not.called" // ownership passed to list of
// JarEntryAnnotationFileResource, where `file` appears in every element of the list
})
private static void addAnnotationFilesToList(
File location, List 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 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, Comparator.comparing(File::getName));
for (File enclosed : directoryContents) {
addAnnotationFilesToList(enclosed, resources, fileType);
}
}
}
/**
* Returns true if the given {@link ExecutableElement} is the canonical constructor of a record
* (i.e., the parameter types of the constructor correspond to the parameter types of the record
* components, ignoring annotations).
*
* @param elt the constructor/method to check
* @param types the Types instance to use for comparing types
* @return true if elt is the canonical constructor of the record containing it
*/
public static boolean isCanonicalConstructor(ExecutableElement elt, Types types) {
if (elt.getKind() != ElementKind.CONSTRUCTOR) {
return false;
}
TypeElement enclosing = (TypeElement) elt.getEnclosingElement();
if (!ElementUtils.isRecordElement(enclosing)) {
return false;
}
List extends Element> recordComponents = ElementUtils.getRecordComponents(enclosing);
if (recordComponents.size() == elt.getParameters().size()) {
for (int i = 0; i < recordComponents.size(); i++) {
if (!types.isSameType(
recordComponents.get(i).asType(), elt.getParameters().get(i).asType())) {
return false;
}
}
return true;
}
return false;
}
}