blob: fa8a377d4dda555c7ec6acb2bddb97db9251fe57 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.rmtypedefs;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.objectweb.asm.Opcodes.ASM5;
/**
* Finds and deletes typedef annotation classes (and also warns if their
* retention was wrong, such that uses embeds
*/
public class RmTypeDefs {
private static final String ANNOTATION = "java/lang/annotation/Annotation";
private static final String STRING_DEF = "android/annotation/StringDef";
private static final String INT_DEF = "android/annotation/IntDef";
private static final String INT_DEF_DESC = "L" + INT_DEF + ";";
private static final String STRING_DEF_DESC = "L" + STRING_DEF + ";";
private static final String RETENTION_DESC = "Ljava/lang/annotation/Retention;";
private static final String RETENTION_POLICY_DESC = "Ljava/lang/annotation/RetentionPolicy;";
private static final String SOURCE_RETENTION_VALUE = "SOURCE";
private boolean mQuiet;
private boolean mVerbose;
private boolean mHaveError;
private boolean mDryRun;
private Set<String> mAnnotationNames = Sets.newHashSet();
private List<File> mAnnotationClassFiles = Lists.newArrayList();
private Set<File> mAnnotationOuterClassFiles = Sets.newHashSet();
public static void main(String[] args) {
new RmTypeDefs().run(args);
}
private void run(String[] args) {
if (args.length == 0) {
usage(System.err);
System.exit(1);
}
List<File> dirs = new ArrayList<File>();
for (String arg : args) {
if (arg.equals("--help") || arg.equals("-h")) {
usage(System.out);
return;
} else if (arg.equals("-q") || arg.equals("--quiet") || arg.equals("--silent")) {
mQuiet = true;
} else if (arg.equals("-v") || arg.equals("--verbose")) {
mVerbose = true;
} else if (arg.equals("-n") || arg.equals("--dry-run")) {
mDryRun = true;
} else if (arg.startsWith("-")) {
System.err.println("Unknown argument " + arg);
usage(System.err);
System.exit(1);
} else {
// Other arguments should be file names
File file = new File(arg);
if (file.exists()) {
dirs.add(file);
} else {
System.err.println(file + " does not exist");
usage(System.err);
System.exit(1);
}
}
}
if (!mQuiet) {
System.out.println("Deleting @IntDef and @StringDef annotation class files");
}
// Record typedef annotation names and files
for (File dir : dirs) {
checkFile(dir);
}
// Rewrite the .class files for any classes that *contain* typedefs as innerclasses
rewriteOuterClasses();
// Removes the actual .class files for the typedef annotations
deleteAnnotationClasses();
System.exit(mHaveError ? -1 : 0);
}
/**
* Visits the given directory tree recursively and calls {@link #checkClass(java.io.File)}
* for any .class files encountered
*/
private void checkFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
checkFile(f);
}
}
} else if (file.isFile()) {
String path = file.getPath();
if (path.endsWith(".class")) {
checkClass(file);
} else if (path.endsWith(".jar")) {
System.err.println(path + ": Warning: Encountered .jar file; .class files "
+ "are not scanned and removed inside .jar files");
}
}
}
/**
* Checks the given .class file to see if it's a typedef annotation, and if so
* records that fact by calling {@link #addTypeDef(String, java.io.File)}
*/
private void checkClass(File file) {
try {
byte[] bytes = Files.toByteArray(file);
ClassReader classReader = new ClassReader(bytes);
classReader.accept(new TypeDefVisitor(file), 0);
} catch (IOException e) {
System.err.println("Could not read " + file + ": " + e.getLocalizedMessage());
System.exit(1);
}
}
/**
* Prints usage statement.
*/
static void usage(PrintStream out) {
out.println("Android TypeDef Remover 1.0");
out.println("Copyright (C) 2013 The Android Open Source Project\n");
out.println("Usage: rmtypedefs folder1 [folder2 [folder3...]]\n");
out.println("Options:");
out.println(" -h,--help show this message");
out.println(" -q,--quiet quiet");
out.println(" -v,--verbose verbose");
out.println(" -n,--dry-run dry-run only, leaves files alone");
out.println(" --verify run extra diagnostics to verify file integrity");
}
/**
* Records the given class name (internal name) and class file path as corresponding to a
* typedef annotation
* */
private void addTypeDef(String name, File file) {
mAnnotationClassFiles.add(file);
mAnnotationNames.add(name);
String fileName = file.getName();
int index = fileName.lastIndexOf('$');
if (index != -1) {
File parentFile = file.getParentFile();
assert parentFile != null : file;
File container = new File(parentFile, fileName.substring(0, index) + ".class");
if (container.exists()) {
mAnnotationOuterClassFiles.add(container);
} else {
System.err.println("Warning: Could not find outer class " + container
+ " for typedef " + file);
mHaveError = true;
}
}
}
/**
* Rewrites the outer classes containing the typedefs such that they no longer refer to
* the (now removed) typedef annotation inner classes
*/
private void rewriteOuterClasses() {
for (File file : mAnnotationOuterClassFiles) {
byte[] bytes;
try {
bytes = Files.toByteArray(file);
} catch (IOException e) {
System.err.println("Could not read " + file + ": " + e.getLocalizedMessage());
mHaveError = true;
continue;
}
ClassWriter classWriter = new ClassWriter(ASM5);
ClassVisitor classVisitor = new ClassVisitor(ASM5, classWriter) {
@Override
public void visitInnerClass(String name, String outerName, String innerName,
int access) {
if (!mAnnotationNames.contains(name)) {
super.visitInnerClass(name, outerName, innerName, access);
}
}
};
ClassReader reader = new ClassReader(bytes);
reader.accept(classVisitor, 0);
byte[] rewritten = classWriter.toByteArray();
try {
Files.write(rewritten, file);
} catch (IOException e) {
System.err.println("Could not write " + file + ": " + e.getLocalizedMessage());
mHaveError = true;
//noinspection UnnecessaryContinue
continue;
}
}
}
/**
* Performs the actual deletion (or display, if in dry-run mode) of the typedef annotation
* files
*/
private void deleteAnnotationClasses() {
for (File mFile : mAnnotationClassFiles) {
if (mVerbose) {
if (mDryRun) {
System.out.println("Would delete " + mFile);
} else {
System.out.println("Deleting " + mFile);
}
}
if (!mDryRun) {
boolean deleted = mFile.delete();
if (!deleted) {
System.err.println("Could not delete " + mFile);
mHaveError = true;
}
}
}
}
/**
* Visitor which visits .class files and checks whether each class is a typedef annotation
* (and if so, calls {@link #addTypeDef(String, java.io.File)}
*/
private class TypeDefVisitor extends ClassVisitor {
/** Class file name */
private File mFile;
/** Class name */
private String mName;
/** Is this class an annotation? */
private boolean mAnnotation;
/** Is this annotation a typedef? Only applies if {@link #mAnnotation} */
private boolean mTypedef;
/** Does the annotation have source retention? Only applies if {@link #mAnnotation} */
private boolean mSourceRetention;
public TypeDefVisitor(File file) {
super(ASM5);
mFile = file;
}
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
mName = name;
mAnnotation = interfaces != null && interfaces.length >= 1
&& ANNOTATION.equals(interfaces[0]);
// Special case: Also delete the actual @IntDef and @StringDef .class files.
// These have class file retention
mTypedef = name.equals(INT_DEF) || name.equals(STRING_DEF);
}
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
mTypedef = desc.equals(INT_DEF_DESC) || desc.equals(STRING_DEF_DESC);
if (desc.equals(RETENTION_DESC)) {
return new AnnotationVisitor(ASM5) {
public void visitEnum(String name, String desc, String value) {
if (desc.equals(RETENTION_POLICY_DESC)) {
mSourceRetention = SOURCE_RETENTION_VALUE.equals(value);
}
}
};
}
return null;
}
public void visitEnd() {
if (mAnnotation && mTypedef) {
if (!mSourceRetention && !mName.equals(STRING_DEF) && !mName.equals(INT_DEF)) {
System.err.println(mFile + ": Warning: Annotation should be annotated "
+ "with @Retention(RetentionPolicy.SOURCE)");
mHaveError = true;
}
addTypeDef(mName, mFile);
}
}
}
}