blob: dad0e8b719a1c5bde84f89627c2472d053ea71c5 [file] [log] [blame]
package org.checkerframework.framework.stub;
import com.github.javaparser.ParseException;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.StubUnit;
import com.github.javaparser.ast.body.AnnotationDeclaration;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.EnumConstantDeclaration;
import com.github.javaparser.ast.body.EnumDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.InitializerDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.body.ReceiverParameter;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.expr.VariableDeclarationExpr;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.type.ArrayType;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.ReferenceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.type.VoidType;
import com.github.javaparser.ast.type.WildcardType;
import com.github.javaparser.ast.visitor.GenericVisitorAdapter;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.checker.signature.qual.ClassGetName;
import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.framework.util.JavaParserUtil;
import org.checkerframework.javacutil.BugInCF;
import org.plumelib.reflection.Signatures;
import scenelib.annotations.Annotation;
import scenelib.annotations.el.AClass;
import scenelib.annotations.el.ADeclaration;
import scenelib.annotations.el.AElement;
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.BoundLocation;
import scenelib.annotations.el.DefException;
import scenelib.annotations.el.LocalLocation;
import scenelib.annotations.el.TypePathEntry;
import scenelib.annotations.io.IndexFileParser;
import scenelib.annotations.io.IndexFileWriter;
/**
* Convert a JAIF file plus a stub file into index files (JAIFs). Note that the resulting index
* files will not include annotation definitions, for which stubfiles do not generally provide
* complete information.
*
* <p>An instance of the class represents conversion of 1 stub file, but the static {@link
* #main(String[])} method converts multiple stub files, instantiating the class multiple times.
*/
public class ToIndexFileConverter extends GenericVisitorAdapter<Void, AElement> {
// The possessive modifiers "*+" are for efficiency only.
// private static Pattern packagePattern =
// Pattern.compile("\\bpackage *+((?:[^.]*+[.] *+)*+[^ ]*) *+;");
private static Pattern importPattern =
Pattern.compile("\\bimport *+((?:[^.]*+[.] *+)*+[^ ]*) *+;");
/**
* Package name that is active at the current point in the input file. Changes as package
* declarations are encountered.
*/
private final @DotSeparatedIdentifiers String pkgName;
/** Imports that appear in the stub file. */
private final List<String> imports;
/** A scene read from the input JAIF file, and will be written to the output JAIF file. */
private final AScene scene;
/**
* @param pkgDecl AST node for package declaration
* @param importDecls AST nodes for import declarations
* @param scene scene for visitor methods to fill in
*/
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/658 for getNameAsString
public ToIndexFileConverter(
@Nullable PackageDeclaration pkgDecl, List<ImportDeclaration> importDecls, AScene scene) {
this.scene = scene;
pkgName = pkgDecl == null ? null : pkgDecl.getNameAsString();
if (importDecls == null) {
imports = Collections.emptyList();
} else {
ArrayList<String> imps = new ArrayList<>(importDecls.size());
for (ImportDeclaration decl : importDecls) {
if (!decl.isStatic()) {
Matcher m = importPattern.matcher(decl.toString());
if (m.find()) {
String s = m.group(1);
if (s != null) {
imps.add(s);
}
}
}
}
imps.trimToSize();
imports = Collections.unmodifiableList(imps);
}
}
/**
* Parse stub files and write out equivalent JAIFs. Note that the results do not include
* annotation definitions, for which stubfiles do not generally provide complete information.
*
* @param args name of JAIF with annotation definition, followed by names of stub files to be
* converted (if none given, program reads from standard input)
*/
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("usage: java ToIndexFileConverter myfile.jaif [stubfile...]");
System.err.println("(myfile.jaif contains needed annotation definitions)");
System.exit(1);
}
AScene scene = new AScene();
try {
// args[0] is a jaif file with needed annotation definitions
IndexFileParser.parseFile(args[0], scene);
if (args.length == 1) {
convert(scene, System.in, System.out);
return;
}
for (int i = 1; i < args.length; i++) {
String f0 = args[i];
String f1 = (f0.endsWith(".astub") ? f0.substring(0, f0.length() - 6) : f0) + ".jaif";
try (InputStream in = new FileInputStream(f0);
OutputStream out = new FileOutputStream(f1); ) {
convert(new AScene(scene), in, out);
}
}
} catch (Throwable e) {
e.printStackTrace();
System.exit(1);
}
}
/**
* Augment given scene with information from stubfile, reading stubs from input stream and writing
* JAIF to output stream.
*
* @param scene the initial scene
* @param in stubfile contents
* @param out JAIF representing augmented scene
* @throws ParseException if the stub file cannot be parsed
* @throws DefException if two different definitions of the same annotation cannot be unified
* @throws IOException if there is trouble with file reading or writing
*/
private static void convert(AScene scene, InputStream in, OutputStream out)
throws IOException, DefException, ParseException {
StubUnit iu;
try {
iu = JavaParserUtil.parseStubUnit(in);
} catch (ParseProblemException e) {
iu = null;
throw new BugInCF(
"ToIndexFileConverter: exception from JavaParser.parseStubUnit for InputStream."
+ System.lineSeparator()
+ "Problem message with problems encountered: "
+ e.getMessage());
}
extractScene(iu, scene);
try (Writer w = new BufferedWriter(new OutputStreamWriter(out))) {
IndexFileWriter.write(scene, w);
}
}
/**
* Entry point of recursive-descent IndexUnit to AScene transformer. It operates by visiting the
* stub and scene in parallel, descending into them in the same way. It augments the existing
* scene (it does not create a new scene).
*
* @param iu {@link StubUnit} representing stubfile
*/
private static void extractScene(StubUnit iu, AScene scene) {
for (CompilationUnit cu : iu.getCompilationUnits()) {
NodeList<TypeDeclaration<?>> typeDecls = cu.getTypes();
if (typeDecls != null && cu.getPackageDeclaration().isPresent()) {
List<ImportDeclaration> impDecls = cu.getImports();
PackageDeclaration pkgDecl = cu.getPackageDeclaration().get();
for (TypeDeclaration<?> typeDecl : typeDecls) {
ToIndexFileConverter converter = new ToIndexFileConverter(pkgDecl, impDecls, scene);
String pkgName = converter.pkgName;
String name = typeDecl.getNameAsString();
if (pkgName != null) {
name = pkgName + "." + name;
}
typeDecl.accept(converter, scene.classes.getVivify(name));
}
}
}
}
/**
* Builds simplified annotation from its declaration. Only the name is included, because stubfiles
* do not generally have access to the full definitions of annotations.
*/
private static Annotation extractAnnotation(AnnotationExpr expr) {
String exprName = expr.toString().substring(1); // leave off leading '@'
// Eliminate jdk.Profile+Annotation, a synthetic annotation that
// the JDK adds, apparently for profiling.
if (exprName.contains("+")) {
return null;
}
@SuppressWarnings("signature") // special case for annotations containing "+"
AnnotationDef def =
new AnnotationDef(exprName, "ToIndexFileConverter.extractAnnotation(" + expr + ")");
def.setFieldTypes(Collections.emptyMap());
return new Annotation(def, Collections.emptyMap());
}
@Override
public Void visit(AnnotationDeclaration decl, AElement elem) {
return null;
}
@Override
public Void visit(BlockStmt stmt, AElement elem) {
return null;
// super.visit(stmt, elem);
}
@Override
public Void visit(ClassOrInterfaceDeclaration decl, AElement elem) {
visitDecl(decl, (ADeclaration) elem);
return super.visit(decl, elem);
}
@Override
public Void visit(ConstructorDeclaration decl, AElement elem) {
List<Parameter> params = decl.getParameters();
List<AnnotationExpr> rcvrAnnos = decl.getAnnotations();
BlockStmt body = decl.getBody();
StringBuilder sb = new StringBuilder("<init>(");
AClass clazz = (AClass) elem;
AMethod method;
// Some of the methods in the generated parser use null to represent an empty list.
if (params != null) {
for (Parameter param : params) {
Type ptype = param.getType();
sb.append(getJVML(ptype));
}
}
sb.append(")V");
method = clazz.methods.getVivify(sb.toString());
visitDecl(decl, method);
if (params != null) {
for (int i = 0; i < params.size(); i++) {
Parameter param = params.get(i);
AField field = method.parameters.getVivify(i);
visitType(param.getType(), field.type);
}
}
if (rcvrAnnos != null) {
for (AnnotationExpr expr : rcvrAnnos) {
Annotation anno = extractAnnotation(expr);
method.receiver.tlAnnotationsHere.add(anno);
}
}
return body == null ? null : body.accept(this, method);
// return super.visit(decl, elem);
}
@Override
public Void visit(EnumConstantDeclaration decl, AElement elem) {
AField field = ((AClass) elem).fields.getVivify(decl.getNameAsString());
visitDecl(decl, field);
return super.visit(decl, field);
}
@Override
public Void visit(EnumDeclaration decl, AElement elem) {
visitDecl(decl, (ADeclaration) elem);
return super.visit(decl, elem);
}
@Override
public Void visit(FieldDeclaration decl, AElement elem) {
for (VariableDeclarator v : decl.getVariables()) {
AClass clazz = (AClass) elem;
AField field = clazz.fields.getVivify(v.getNameAsString());
visitDecl(decl, field);
visitType(decl.getCommonType(), field.type);
}
return null;
}
@Override
public Void visit(InitializerDeclaration decl, AElement elem) {
BlockStmt block = decl.getBody();
AClass clazz = (AClass) elem;
block.accept(this, clazz.methods.getVivify(decl.isStatic() ? "<clinit>" : "<init>"));
return null;
}
@Override
public Void visit(MethodDeclaration decl, AElement elem) {
Type type = decl.getType();
List<Parameter> params = decl.getParameters();
List<TypeParameter> typeParams = decl.getTypeParameters();
Optional<ReceiverParameter> rcvrParam = decl.getReceiverParameter();
BlockStmt body = decl.getBody().orElse(null);
StringBuilder sb = new StringBuilder(decl.getNameAsString()).append('(');
AClass clazz = (AClass) elem;
AMethod method;
if (params != null) {
for (Parameter param : params) {
Type ptype = param.getType();
sb.append(getJVML(ptype));
}
}
sb.append(')').append(getJVML(type));
method = clazz.methods.getVivify(sb.toString());
visitDecl(decl, method);
visitType(type, method.returnType);
if (params != null) {
for (int i = 0; i < params.size(); i++) {
Parameter param = params.get(i);
AField field = method.parameters.getVivify(i);
visitType(param.getType(), field.type);
}
}
if (rcvrParam.isPresent()) {
for (AnnotationExpr expr : rcvrParam.get().getAnnotations()) {
Annotation anno = extractAnnotation(expr);
method.receiver.type.tlAnnotationsHere.add(anno);
}
}
if (typeParams != null) {
for (int i = 0; i < typeParams.size(); i++) {
TypeParameter typeParam = typeParams.get(i);
List<ClassOrInterfaceType> bounds = typeParam.getTypeBound();
if (bounds != null) {
for (int j = 0; j < bounds.size(); j++) {
ClassOrInterfaceType bound = bounds.get(j);
BoundLocation loc = new BoundLocation(i, j);
bound.accept(this, method.bounds.getVivify(loc));
}
}
}
}
return body == null ? null : body.accept(this, method);
}
@Override
public Void visit(ObjectCreationExpr expr, AElement elem) {
ClassOrInterfaceType type = expr.getType();
AClass clazz = scene.classes.getVivify(type.getNameAsString());
Expression scope = expr.getScope().orElse(null);
List<Type> typeArgs = expr.getTypeArguments().orElse(null);
List<Expression> args = expr.getArguments();
NodeList<BodyDeclaration<?>> bodyDecls = expr.getAnonymousClassBody().orElse(null);
if (scope != null) {
scope.accept(this, elem);
}
if (args != null) {
for (Expression arg : args) {
arg.accept(this, elem);
}
}
if (typeArgs != null) {
for (Type typeArg : typeArgs) {
typeArg.accept(this, elem);
}
}
type.accept(this, clazz);
if (bodyDecls != null) {
for (BodyDeclaration<?> decl : bodyDecls) {
decl.accept(this, clazz);
}
}
return null;
}
@Override
public Void visit(VariableDeclarationExpr expr, AElement elem) {
List<AnnotationExpr> annos = expr.getAnnotations();
AMethod method = (AMethod) elem;
List<VariableDeclarator> varDecls = expr.getVariables();
for (int i = 0; i < varDecls.size(); i++) {
VariableDeclarator decl = varDecls.get(i);
LocalLocation loc = new LocalLocation(i, decl.getNameAsString());
AField field = method.body.locals.getVivify(loc);
visitType(expr.getCommonType(), field.type);
if (annos != null) {
for (AnnotationExpr annoExpr : annos) {
Annotation anno = extractAnnotation(annoExpr);
field.tlAnnotationsHere.add(anno);
}
}
}
return null;
}
/**
* Copies information from an AST declaration node to an {@link ADeclaration}. Called by visitors
* for BodyDeclaration subclasses.
*/
private Void visitDecl(BodyDeclaration<?> decl, ADeclaration elem) {
NodeList<AnnotationExpr> annoExprs = decl.getAnnotations();
if (annoExprs != null) {
for (AnnotationExpr annoExpr : annoExprs) {
Annotation anno = extractAnnotation(annoExpr);
elem.tlAnnotationsHere.add(anno);
}
}
return null;
}
/** Copies information from an AST type node to an {@link ATypeElement}. */
private Void visitType(Type type, final ATypeElement elem) {
List<AnnotationExpr> exprs = type.getAnnotations();
if (exprs != null) {
for (AnnotationExpr expr : exprs) {
Annotation anno = extractAnnotation(expr);
if (anno != null) {
elem.tlAnnotationsHere.add(anno);
}
}
}
visitInnerTypes(type, elem);
return null;
}
/**
* Copies information from an AST type node's inner type nodes to an {@link ATypeElement}.
*
* @param type AST Type node to inspect
* @param elem destination type element
*/
private static Void visitInnerTypes(Type type, final ATypeElement elem) {
return type.accept(
new GenericVisitorAdapter<Void, List<TypePathEntry>>() {
@Override
public Void visit(ClassOrInterfaceType type, List<TypePathEntry> loc) {
if (type.getTypeArguments().isPresent()) {
List<Type> typeArgs = type.getTypeArguments().get();
for (int i = 0; i < typeArgs.size(); i++) {
Type inner = typeArgs.get(i);
List<TypePathEntry> ext = extendedTypePath(loc, 3, i);
visitInnerType(inner, ext);
}
}
return null;
}
@Override
public Void visit(ArrayType type, List<TypePathEntry> loc) {
List<TypePathEntry> ext = loc;
int n = type.getArrayLevel();
Type currentType = type;
for (int i = 0; i < n; i++) {
ext = extendedTypePath(ext, 1, 0);
for (AnnotationExpr expr : currentType.getAnnotations()) {
ATypeElement typeElem = elem.innerTypes.getVivify(ext);
Annotation anno = extractAnnotation(expr);
typeElem.tlAnnotationsHere.add(anno);
}
currentType =
((com.github.javaparser.ast.type.ArrayType) currentType).getComponentType();
}
return null;
}
@Override
public Void visit(WildcardType type, List<TypePathEntry> loc) {
ReferenceType lower = type.getExtendedType().orElse(null);
ReferenceType upper = type.getSuperType().orElse(null);
if (lower != null) {
List<TypePathEntry> ext = extendedTypePath(loc, 2, 0);
visitInnerType(lower, ext);
}
if (upper != null) {
List<TypePathEntry> ext = extendedTypePath(loc, 2, 0);
visitInnerType(upper, ext);
}
return null;
}
/** Copies information from an AST inner type node to an {@link ATypeElement}. */
private void visitInnerType(Type type, List<TypePathEntry> loc) {
ATypeElement typeElem = elem.innerTypes.getVivify(loc);
for (AnnotationExpr expr : type.getAnnotations()) {
Annotation anno = extractAnnotation(expr);
typeElem.tlAnnotationsHere.add(anno);
type.accept(this, loc);
}
}
/**
* Extends type path by one element.
*
* @see TypePathEntry(int, int)
*/
private List<TypePathEntry> extendedTypePath(List<TypePathEntry> loc, int tag, int arg) {
List<TypePathEntry> path = new ArrayList<>(loc.size() + 1);
path.addAll(loc);
path.add(TypePathEntry.create(tag, arg));
return path;
}
},
Collections.emptyList());
}
/**
* Computes a type's "binary name".
*
* @param type the type
* @return the type's binary name
*/
private String getJVML(Type type) {
return type.accept(
new GenericVisitorAdapter<String, Void>() {
@Override
public String visit(ClassOrInterfaceType type, Void v) {
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/658 for getNameAsString
@FullyQualifiedName String typeName = type.getNameAsString();
@SuppressWarnings("signature" // TODO: bug in ToIndexFileConverter:
// resolve requires a @BinaryName, but this passes a @FullyQualifiedName.
// They differ for inner classes.
)
String name = resolve(typeName);
if (name == null) {
// could be defined in the same stub file
return "L" + typeName + ";";
}
return "L" + String.join("/", name.split("\\.")) + ";";
}
@Override
public String visit(PrimitiveType type, Void v) {
switch (type.getType()) {
case BOOLEAN:
return "Z";
case BYTE:
return "B";
case CHAR:
return "C";
case DOUBLE:
return "D";
case FLOAT:
return "F";
case INT:
return "I";
case LONG:
return "J";
case SHORT:
return "S";
default:
throw new BugInCF("unknown primitive type " + type);
}
}
@Override
public String visit(ArrayType type, Void v) {
String typeName = type.getElementType().accept(this, null);
StringBuilder sb = new StringBuilder();
int n = type.getArrayLevel();
for (int i = 0; i < n; i++) {
sb.append("[");
}
sb.append(typeName);
return sb.toString();
}
@Override
public String visit(VoidType type, Void v) {
return "V";
}
@Override
public String visit(WildcardType type, Void v) {
return type.getSuperType().get().accept(this, null);
}
},
null);
}
/**
* Finds the fully qualified name of the class with the given name.
*
* @param className possibly unqualified name of class
* @return fully qualified name of class that {@code className} identifies in the current context,
* or null if resolution fails
*/
private @BinaryName String resolve(@BinaryName String className) {
if (pkgName != null) {
String qualifiedName = Signatures.addPackage(pkgName, className);
if (loadClass(qualifiedName) != null) {
return qualifiedName;
}
}
{
// Every Java program implicitly does "import java.lang.*",
// so see whether this class is in that package.
String qualifiedName = Signatures.addPackage("java.lang", className);
if (loadClass(qualifiedName) != null) {
return qualifiedName;
}
}
for (String declName : imports) {
String qualifiedName = mergeImport(declName, className);
if (loadClass(qualifiedName) != null) {
return qualifiedName;
}
}
if (loadClass(className) != null) {
return className;
}
return null;
}
/**
* Combines an import with a partial binary name, yielding a binary name.
*
* @param importName package name or (for an inner class) the outer class name
* @param className the class name
* @return fully qualified class name if resolution succeeds, null otherwise
*/
@SuppressWarnings("signature") // string manipulation of signature strings
private static @BinaryName String mergeImport(String importName, @BinaryName String className) {
if (importName.isEmpty() || importName.equals(className)) {
return className;
}
String[] importSplit = importName.split("\\.");
String[] classSplit = className.split("\\.");
String importEnd = importSplit[importSplit.length - 1];
if ("*".equals(importEnd)) {
return importName.substring(0, importName.length() - 1) + className;
} else {
// find overlap such as in
// import a.b.C.D;
// C.D myvar;
int i = importSplit.length;
int n = i - classSplit.length;
while (--i >= n) {
if (!classSplit[i - n].equals(importSplit[i])) {
return null;
}
}
return importName;
}
}
/**
* Finds {@link Class} corresponding to a name.
*
* @param className a class name
* @return {@link Class} object corresponding to className, or null if none found
*/
private static Class<?> loadClass(@ClassGetName String className) {
assert className != null;
try {
return Class.forName(className, false, null);
} catch (ClassNotFoundException e) {
return null;
}
}
}