package checkers.util.stub; import checkers.nullness.quals.*; import java.io.*; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.objectweb.asm.*; import org.objectweb.asm.commons.*; import org.objectweb.asm.signature.*; import org.objectweb.asm.util.*; /** * Generates a "skeleton" class, in which the bodies of non-abstract methods * are replaced by a stub code (in this case, throwing a {@link * RuntimeException}). * * <p> * * This class is useful for adding annotations to the public fields and method * signatures of libraries. A programmer may generate skeleton classes for a * library using this tool and annotate its methods and fields. Then, a * programmer can compile against these skeleton classes for typechecking (and * must remove them when running the program, so that the stub methods are not * invoked instead of the library's real methods). */ public class Skeleton implements ClassVisitor { /** A writer for outputting the skeleton class. */ private final PrintWriter pw; /** The name of the class. */ private final String name; /** The string for initial indentation. */ private final String base; /** The string for extra indentation. */ private final String tab; /** A buffer for locally storing output. */ private final StringBuffer buf; /** A queue of strings to output (after ASM's TraceClassVisitor). */ private final List<String> text; /** A set of class names that have already been visited and can be skipped. */ private final Set<String> visited; private boolean aborted = false; private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private boolean isEnum = false; /** * Creates a new skeleton class generator. * * @param name the name of the class * @param base the initial indentation * @param tab the extra indentation * @param pw the writer for outputting the skeleton class * @param visited the set of classes that have already been visited */ public Skeleton(String name, String base, String tab, PrintWriter pw, Set<String> visited) { this.pw = pw; this.name = toClassName(name); this.base = base; this.tab = tab; this.buf = new StringBuffer(); this.text = new LinkedList<String>(); if (visited == null) this.visited = new HashSet<String>(); else this.visited = visited; } /* * Prints the skeleton class for the class named by the first command-line * argument to standard out, including its package definition. */ public static void main(final String[] args) throws Exception { if (args.length == 0 || args.length > 2) { System.err.println("usage: Skeleton [class name] [jar file]"); return; } String baseDir = null; if (args.length == 1) { PrintWriter pw = new PrintWriter(System.out); Class<?> cls = Class.forName(args[0]); pw.println("package " + cls.getPackage().getName() + ";"); pw.println(); Skeleton.read(args[0], "", " ", pw, new HashSet<String>()); } else if (args.length == 2) { List<String> classes = getFiles(args[0], args[1], true); for (String className : classes) { try { createDirForClass(className, baseDir); Class<?> cls = Class.forName(className); PrintWriter pw = new PrintWriter(new File(getClassFileName(className))); pw.println("package " + cls.getPackage().getName() + ";"); pw.println(); Skeleton.read(className, "", " ", pw, new HashSet<String>()); System.out.println("Output : " + className); } catch (Exception e) { e.printStackTrace(); } catch (Error e) { e.printStackTrace(); } } } } public static String getClassFileName(String className) { String classFileName = className.replace(".", "/"); classFileName = classFileName + ".java"; return classFileName; } public static boolean createDirForClass(String className, String baseDir) { // Ignore className String finalDirectory = className.substring(0, className.lastIndexOf('.')); finalDirectory = finalDirectory.replace('.', '/'); if (baseDir != null) { if (baseDir.endsWith("/")) finalDirectory = baseDir + finalDirectory; else finalDirectory = baseDir + '/' + finalDirectory; } return new File(finalDirectory).mkdirs(); } public static List<String> getFiles(String packageName, String jar, boolean subpackage) throws Exception { List<String> results = new ArrayList<String>(); packageName = packageName.replaceAll("\\.$", ""); if (!jar.endsWith(".jar")) return Collections.<String>emptyList(); JarFile jarFile = new JarFile(jar); for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements(); ) { JarEntry entry = e.nextElement(); String entryName = entry.getName(); if (entryName.endsWith(".class")) { String className = entryName.replaceAll("\\.class$", "").replace("/", "."); if (className.contains("$")) { // inner class continue; } if (".".equals(packageName) || className.startsWith(packageName)) { if (!subpackage && className.substring(packageName.length() + 1).contains(".")) continue; results.add(className); } } } return results; } /** * Reads a class with the given name and prints its skeleton to the given * {@link PrintWriter}. * * @param className the name of the class for which to generate a skeleton * @param base the initial indentation * @param tab the extra indentation * @param pw the writer on which to output the skeleton * @param visited the set of visited classes that should not be read */ public static void read(final String className, final String base, final String tab, PrintWriter pw, /*@NonNull*/ Set<String> visited) throws Exception { if (visited.contains(className)) return; ClassReader cr = new ClassReader(className); visited.add(className); cr.accept(new Skeleton(className, base, tab, pw, visited), false); } /** * Reads a class with the given name and prints its skeleton to the given * {@link PrintWriter}. * * @param className the name of the class for which to generate a skeleton * @param base the initial indentation * @param tab the extra indentation * @param pw the writer on which to output the skeleton * @param visited the set of visited classes that should not be read */ public static void read(final InputStream classStream, String className, final String base, final String tab, PrintWriter pw, /*@NonNull*/ Set<String> visited) throws Exception { if (visited.contains(className)) return; ClassReader cr = new ClassReader(classStream); visited.add(className); cr.accept(new Skeleton(className, base, tab, pw, visited), false); } /** * A class that prints a Java signature from a JVM signature with named * arguments. */ static class ArgumentSignatureVisitor extends TraceSignatureVisitor { private int pos = 0; private int arg = 0; private boolean finished = true; private boolean skip = false; private int skipCount = 0; private final StringBuffer local; /** * Creates an {@link ArgumentSignatureVisitor} for a signature with the * given flags. * * @param access the signature's flags */ public ArgumentSignatureVisitor(int access) { super(access); local = new StringBuffer(); } /** * @return the parameter string with named arguments */ public String getParams() { return local.toString(); } /** * Updates the buffer with the parent's buffer, possibly adding a named * argument. * * @param addArg whether or not a named argument should be added */ private void updateLocal(boolean addArg) { if (finished || skip) return; local.append(super.getDeclaration().substring(pos)); pos = super.getDeclaration().length(); if (addArg) local.append(" a" + (++arg)); } @Override public SignatureVisitor visitParameterType() { finished = false; super.visitParameterType(); local.append(super.getDeclaration().substring(pos)); pos = super.getDeclaration().length(); return this; } @Override public SignatureVisitor visitReturnType() { super.visitReturnType(); updateLocal(false); finished = true; return this; } @Override public void visitBaseType(char descriptor) { super.visitBaseType(descriptor); updateLocal(true); } @Override public void visitClassType(String name) { if (skip == true) skipCount++; super.visitClassType(name); } @Override public void visitTypeArgument() { super.visitTypeArgument(); } @Override public SignatureVisitor visitTypeArgument(char tag) { skip = true; super.visitTypeArgument(tag); return this; } @Override public void visitTypeVariable(String name) { super.visitTypeVariable(name); updateLocal(true); } @Override public void visitEnd() { skip = false; super.visitEnd(); updateLocal(skipCount == 0); if (skipCount > 0) skipCount--; } } /** * Appends modifiers to the local buffer from the given set of flags. * * @param access the modifier flags * @param suppress flags for which no modifier should be added */ private void appendAccess(final int access, final int suppress) { if (((access & ~suppress) & Opcodes.ACC_PUBLIC) != 0) buf.append("public "); if (((access & ~suppress) & Opcodes.ACC_PRIVATE) != 0) buf.append("private "); if (((access & ~suppress) & Opcodes.ACC_PROTECTED) != 0) buf.append("protected "); if (((access & ~suppress) & Opcodes.ACC_FINAL) != 0 && ((access & ~suppress) & Opcodes.ACC_ENUM) == 0) buf.append("final "); if (((access & ~suppress) & Opcodes.ACC_STATIC) != 0) buf.append("static "); if (((access & ~suppress) & Opcodes.ACC_SYNCHRONIZED) != 0) buf.append("synchronized "); if (((access & ~suppress) & Opcodes.ACC_VOLATILE) != 0) buf.append("volatile "); if (((access & ~suppress) & Opcodes.ACC_TRANSIENT) != 0) buf.append("transient "); if (((access & ~suppress) & Opcodes.ACC_NATIVE) != 0) buf.append("native "); if (((access & ~suppress) & Opcodes.ACC_ABSTRACT) != 0) buf.append("abstract "); if (((access & ~suppress) & Opcodes.ACC_STRICT) != 0) buf.append("strictfp "); if (((access & ~suppress) & Opcodes.ACC_SYNTHETIC) != 0) buf.append("synthetic "); } /** * Appends the correct keyword for the type of class (class, interface, * annotation, enum). * * @param flags the modifier flags */ private void appendType(final int flags) { if ((flags & Opcodes.ACC_ANNOTATION) != 0) buf.append("@interface"); else if ((flags & Opcodes.ACC_INTERFACE) != 0) buf.append("interface "); else if ((flags & Opcodes.ACC_ENUM) != 0) buf.append("enum "); else buf.append("class "); } /** * Appends the class name to the local buffer. * * @param name the class name */ private void appendName(final String name) { buf.append(toClassName(name)); } /** * Given signatures with and without generics as passed to the visitor (one * or both of which may be null), determines which one should be used for * printing. * * @param signature the signature with generics * @param desc the signature without generics * @return the signature that should be used, or null if neither should */ private final String chooseSignature(final String signature, final String desc) { if (signature != null) return signature; if (desc != null) return desc; return null; } /** * Appends to the local buffer the type variables for a class or field * declaration. * * @param access the modifier flags * @param signature the signature with generics */ private void appendGenerics(final int access, final String signature) { if (signature == null) return; TraceSignatureVisitor sv = new TraceSignatureVisitor(access); SignatureReader r = new SignatureReader(signature); r.accept(sv); buf.append(toClassName(sv.getDeclaration())).append(" "); } private void appendGenericsField(final int access, final String signature) { if (signature == null) return; TraceSignatureVisitor sv = new TraceSignatureVisitor(access); SignatureReader r = new SignatureReader(signature); r.accept(sv); // Hack! String str = toClassName(sv.getDeclaration()); if (str.contains("<")) str = str.substring(str.indexOf("<")); buf.append(str); } /** * Appends to the local buffer the field type for a field declaration. * * @param access the modifier flags * @param desc the field type description */ private void appendFieldType(final int access, final String desc) { if (desc == null) return; TraceSignatureVisitor sv = new TraceSignatureVisitor(access); SignatureReader r = new SignatureReader(desc); r.acceptType(sv); buf.append(adjustType(sv.getDeclaration())); } /** * Appends to the local buffer the return type of a method. * * @param access the modifier flags * @param signature the (possibly null) signature with generics * @param desc the (possibly null) signature without generics */ private void appendReturnType(final int access, final String signature, final String desc) { // Get the correct signature string. String str = chooseSignature(signature, desc); if (str == null) return; // Get the return type from the signature. TraceSignatureVisitor sv = new TraceSignatureVisitor(access); SignatureReader r = new SignatureReader(str); r.accept(sv); String ret = sv.getReturnType(); // Adjust the return type string if necessary. buf.append(adjustType(ret)).append(" "); } private String adjustType(String ret) { String type = ret.trim(); type = type.replace("$", "."); if (type.length() == 0 || type.startsWith("[")) type = "Object" + type; return type; } private String formalAndTypeParams(final int access, final String signature, final String desc) { String str = chooseSignature(signature, desc); if (str == null) return null; ArgumentSignatureVisitor sv = new ArgumentSignatureVisitor(access); SignatureReader r = new SignatureReader(str); r.accept(sv); String params = sv.getParams(); if ((access & Opcodes.ACC_VARARGS) != 0) { // need to replace last argument from an array to ... int index = params.lastIndexOf("[]"); params = params.substring(0, index) + "..." + params.substring(index + 2); } return params; } /** * Appends to the local buffer the type parameters of a method. * * @param access the modifier flags * @param signature the (possibly null) signature with generics * @param desc the (possibly null) signature without generics */ private void appendMethodTypeParams(final int access, final String signature, final String desc) { String params = formalAndTypeParams(access, signature, desc); if (params == null) return; int paren = params.indexOf("("); if (paren > 0) buf.append(params.substring(0, paren)).append(" "); } /** * Appends to the local buffer the parameter type of a method (with named * arguments). * * @param access the modifier flags * @param signature the (possibly null) signature with generics * @param desc the (possibly null) signature without generics */ private void appendParamType(final int access, final String signature, final String desc) { String allparams = formalAndTypeParams(access, signature, desc); if (allparams == null) return; // Adjust param string if necessary. if (allparams.length() == 0) buf.append("()"); else { int paren = allparams.indexOf("("); String params = adjustType(allparams.substring(paren)); buf.append(params); } } public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) { if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0) { this.aborted = true; return; } isEnum = (access & Opcodes.ACC_ENUM) != 0; buf.setLength(0); buf.append(base); appendAccess(access, Opcodes.ACC_SYNCHRONIZED); // If it's an inner class make static if (name.contains("$")) buf.append("static "); appendType(access); appendName(this.name.substring(this.name.lastIndexOf('.')+1)); buf.append(" "); if (signature != null) { if ((access & Opcodes.ACC_ENUM) == 0) appendGenerics(access, signature); } else { if (superName != null && !toClassName(superName).equals("java.lang.Object") && ((access & Opcodes.ACC_ENUM) == 0)) buf.append("extends " + toClassName(superName) + " "); if (interfaces != null && interfaces.length != 0) { buf.append((access & Opcodes.ACC_INTERFACE) == 0 ? "implements " : "extends "); buf.append(toClassName(interfaces[0])); for (int i = 1; i < interfaces.length; ++i) buf.append(", " + toClassName(interfaces[i])); buf.append(" "); } } buf.append("{\n"); text.add(buf.toString()); } public void visitSource(final String file, final String debug) { // Do nothing. } public void visitOuterClass(final String owner, final String name, final String desc) { // Do nothing. } public AnnotationVisitor visitAnnotation( final String desc, final boolean visible) { return new EmptyVisitor(); } public ExtendedAnnotationVisitor visitExtendedAnnotation( final String desc, final boolean visible) { return new EmptyVisitor(); } public void visitAttribute(final Attribute attr) { // Do nothing. } public void visitInnerClass(final String name, final String outerName, final String innerName, final int access) { if (this.aborted) return; if (this.name.equals(name.replace('/', '.')) || !name.replace('/', '.').startsWith(this.name)) return; try { ByteArrayOutputStream ba = new ByteArrayOutputStream(); read(name.replace('/', '.'), base + tab, tab, new PrintWriter(ba), this.visited); buf.setLength(0); buf.append(ba.toString()); } catch (Exception e) { buf.append("// couldn't read inner class ").append(name); } text.add(buf.toString()); } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (this.aborted) return new EmptyVisitor(); if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) { buf.setLength(0); buf.append(base).append(tab); if (!isEnum) { appendAccess(access, Opcodes.ACC_FINAL); appendFieldType(access, desc); appendGenericsField(access, signature); buf.append(" "); } appendName(name); if (value != null) { buf.append(" = "); if (value instanceof String) buf.append("\"").append(value).append("\""); else buf.append(value); } buf.append(isEnum ? ',' : ';'); buf.append(LINE_SEPARATOR); text.add(buf.toString()); } return new EmptyVisitor(); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (this.aborted || name.equals("<clinit>") || name.equals("clone") || (access & Opcodes.ACC_NATIVE) != 0 || (access & Opcodes.ACC_SYNTHETIC) != 0) return new EmptyVisitor(); if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) { buf.setLength(0); buf.append(base).append(tab); if (isEnum && (name.equals("values") || name.equals("valueOf"))) buf.append("//"); appendAccess(access, Opcodes.ACC_TRANSIENT | Opcodes.ACC_VOLATILE); appendMethodTypeParams(access, signature, desc); String methodName; if (name.equals("<init>")) appendName(this.name.substring(this.name.lastIndexOf('.')+1)); else { appendReturnType(access, signature, desc); appendName(name); } appendParamType(access, signature, desc); if (exceptions != null && exceptions.length > 0) { buf.append(" throws "); appendName(exceptions[0]); for (int i = 1; i < exceptions.length; i++) { buf.append(", "); appendName(exceptions[i]); } } if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) buf.append(";"); else buf.append(" { throw new RuntimeException(\"skeleton method\"); }"); buf.append(LINE_SEPARATOR); text.add(buf.toString()); } return new EmptyVisitor(); } public void visitEnd() { if (this.aborted) return; text.add(base); text.add("}"); text.add(LINE_SEPARATOR); for (Object o : text) pw.print(o.toString()); pw.flush(); isEnum = false; } protected String toClassName(final String name) { String result = name; result = name.trim(); result = result.replace("/", "."); result = result.replace("$", "."); return result; } }