diff --git a/tools/rmtypedefs/.idea/copyright/profiles_settings.xml b/tools/rmtypedefs/.idea/copyright/profiles_settings.xml
index 3572571ad..e7bedf337 100644
--- a/tools/rmtypedefs/.idea/copyright/profiles_settings.xml
+++ b/tools/rmtypedefs/.idea/copyright/profiles_settings.xml
@@ -1,5 +1,3 @@
-
-
-
+
\ No newline at end of file
diff --git a/tools/rmtypedefs/.idea/misc.xml b/tools/rmtypedefs/.idea/misc.xml
index 97320410e..863cfc73d 100644
--- a/tools/rmtypedefs/.idea/misc.xml
+++ b/tools/rmtypedefs/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/tools/rmtypedefs/rmtypedefs.iml b/tools/rmtypedefs/rmtypedefs.iml
index 6e0f0fcff..3a594023d 100644
--- a/tools/rmtypedefs/rmtypedefs.iml
+++ b/tools/rmtypedefs/rmtypedefs.iml
@@ -31,6 +31,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/rmtypedefs/src/com/android/tools/rmtypedefs/RmTypeDefs.java b/tools/rmtypedefs/src/com/android/tools/rmtypedefs/RmTypeDefs.java
index 937559036..df7294195 100644
--- a/tools/rmtypedefs/src/com/android/tools/rmtypedefs/RmTypeDefs.java
+++ b/tools/rmtypedefs/src/com/android/tools/rmtypedefs/RmTypeDefs.java
@@ -16,17 +16,21 @@
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.ASM4;
@@ -50,6 +54,10 @@ public class RmTypeDefs {
private boolean mHaveError;
private boolean mDryRun;
+ private Set mAnnotationNames = Sets.newHashSet();
+ private List mAnnotationClassFiles = Lists.newArrayList();
+ private Set mAnnotationOuterClassFiles = Sets.newHashSet();
+
public static void main(String[] args) {
new RmTypeDefs().run(args);
}
@@ -93,19 +101,30 @@ public class RmTypeDefs {
System.out.println("Deleting @IntDef and @StringDef annotation class files");
}
+ // Record typedef annotation names and files
for (File dir : dirs) {
- find(dir);
+ 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);
}
- private void find(File file) {
+ /**
+ * 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) {
- find(f);
+ checkFile(f);
}
}
} else if (file.isFile()) {
@@ -119,11 +138,15 @@ public class RmTypeDefs {
}
}
+ /**
+ * 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 MyVisitor(file), 0);
+ classReader.accept(new TypeDefVisitor(file), 0);
} catch (IOException e) {
System.err.println("Could not read " + file + ": " + e.getLocalizedMessage());
System.exit(1);
@@ -142,9 +165,100 @@ public class RmTypeDefs {
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");
}
- private class MyVisitor extends ClassVisitor {
+ /**
+ * 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(file);
+ } 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(ASM4);
+ ClassVisitor classVisitor = new ClassVisitor(ASM4, 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;
@@ -161,7 +275,7 @@ public class RmTypeDefs {
/** Does the annotation have source retention? Only applies if {@link #mAnnotation} */
private boolean mSourceRetention;
- public MyVisitor(File file) {
+ public TypeDefVisitor(File file) {
super(ASM4);
mFile = file;
}
@@ -203,20 +317,8 @@ public class RmTypeDefs {
+ "with @Retention(RetentionPolicy.SOURCE)");
mHaveError = true;
}
- 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;
- }
- }
+
+ addTypeDef(mName, mFile);
}
}
}
diff --git a/tools/rmtypedefs/test/com/android/tools/rmtypedefs/RmTypeDefsTest.java b/tools/rmtypedefs/test/com/android/tools/rmtypedefs/RmTypeDefsTest.java
new file mode 100644
index 000000000..37a24ea5f
--- /dev/null
+++ b/tools/rmtypedefs/test/com/android/tools/rmtypedefs/RmTypeDefsTest.java
@@ -0,0 +1,254 @@
+package com.android.tools.rmtypedefs;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jdt.core.compiler.batch.BatchCompiler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.Permission;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+@SuppressWarnings("SpellCheckingInspection")
+public class RmTypeDefsTest extends TestCase {
+ public void test() throws IOException {
+ // Creates a test class containing various typedefs, as well as the @IntDef annotation
+ // itself (to make the test case independent of the SDK), and compiles this using
+ // ECJ. It then runs the RmTypeDefs tool on the resulting output directory, and
+ // finally verifies that the tool exits with a 0 exit code.
+
+ File dir = Files.createTempDir();
+ String testClass = ""
+ + "package test.pkg;\n"
+ + "\n"
+ + "import android.annotation.IntDef;\n"
+ + "\n"
+ + "import java.lang.annotation.Retention;\n"
+ + "import java.lang.annotation.RetentionPolicy;\n"
+ + "\n"
+ + "@SuppressWarnings({\"UnusedDeclaration\",\"JavaDoc\"})\n"
+ + "public class TestClass {\n"
+ + " /** @hide */\n"
+ + " @Retention(RetentionPolicy.SOURCE)\n"
+ + " @IntDef(flag = true,\n"
+ + " value = {\n"
+ + " DISPLAY_USE_LOGO,\n"
+ + " DISPLAY_SHOW_HOME,\n"
+ + " DISPLAY_HOME_AS_UP,\n"
+ + " DISPLAY_SHOW_TITLE,\n"
+ + " DISPLAY_SHOW_CUSTOM,\n"
+ + " DISPLAY_TITLE_MULTIPLE_LINES\n"
+ + " })\n"
+ + " public @interface DisplayOptions {}\n"
+ + "\n"
+ + " public static final int DISPLAY_USE_LOGO = 0x1;\n"
+ + " public static final int DISPLAY_SHOW_HOME = 0x2;\n"
+ + " public static final int DISPLAY_HOME_AS_UP = 0x4;\n"
+ + " public static final int DISPLAY_SHOW_TITLE = 0x8;\n"
+ + " public static final int DISPLAY_SHOW_CUSTOM = 0x10;\n"
+ + " public static final int DISPLAY_TITLE_MULTIPLE_LINES = 0x20;\n"
+ + "\n"
+ + " public void setDisplayOptions(@DisplayOptions int options) {\n"
+ + " System.out.println(\"setDisplayOptions \" + options);\n"
+ + " }\n"
+ + " public void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask) {\n"
+ + " System.out.println(\"setDisplayOptions \" + options + \", mask=\" + mask);\n"
+ + " }\n"
+ + "\n"
+ + " public static class StaticInnerClass {\n"
+ + " int mViewFlags = 0;\n"
+ + " static final int VISIBILITY_MASK = 0x0000000C;\n"
+ + "\n"
+ + " /** @hide */\n"
+ + " @IntDef({VISIBLE, INVISIBLE, GONE})\n"
+ + " @Retention(RetentionPolicy.SOURCE)\n"
+ + " public @interface Visibility {}\n"
+ + "\n"
+ + " public static final int VISIBLE = 0x00000000;\n"
+ + " public static final int INVISIBLE = 0x00000004;\n"
+ + " public static final int GONE = 0x00000008;\n"
+ + "\n"
+ + " @Visibility\n"
+ + " public int getVisibility() {\n"
+ + " return mViewFlags & VISIBILITY_MASK;\n"
+ + " }\n"
+ + " }\n"
+ + "\n"
+ + " public static class Inherits extends StaticInnerClass {\n"
+ + " @Override\n"
+ + " @Visibility\n"
+ + " public int getVisibility() {\n"
+ + " return 0;\n"
+ + " }\n"
+ + " }\n"
+ + "}\n";
+ String intdef = ""
+ + "package android.annotation;\n"
+ + "@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS)\n"
+ + "@java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE})\n"
+ + "public @interface IntDef {\n"
+ + " long[] value() default {};\n"
+ + " boolean flag() default false;\n"
+ + "}";
+
+ File srcDir = new File(dir, "test" + File.separator + "pkg");
+ boolean mkdirs = srcDir.mkdirs();
+ assertTrue(mkdirs);
+ File srcFile1 = new File(srcDir, "TestClass.java");
+ Files.write(testClass, srcFile1, Charsets.UTF_8);
+
+ srcDir = new File(dir, "android" + File.separator + "annotation");
+ mkdirs = srcDir.mkdirs();
+ assertTrue(mkdirs);
+ File srcFile2 = new File(srcDir, "IntDef.java");
+ Files.write(intdef, srcFile2, Charsets.UTF_8);
+
+ boolean compileSuccessful = BatchCompiler.compile(srcFile1 + " " + srcFile2 +
+ " -source 1.6 -target 1.6 -nowarn",
+ new PrintWriter(System.out),
+ new PrintWriter(System.err), null);
+ assertTrue(compileSuccessful);
+
+ assertEquals(""
+ + "testDir/\n"
+ + " testDir/android/\n"
+ + " testDir/android/annotation/\n"
+ + " testDir/android/annotation/IntDef.class\n"
+ + " testDir/android/annotation/IntDef.java\n"
+ + " testDir/test/\n"
+ + " testDir/test/pkg/\n"
+ + " testDir/test/pkg/TestClass$DisplayOptions.class\n"
+ + " testDir/test/pkg/TestClass$Inherits.class\n"
+ + " testDir/test/pkg/TestClass$StaticInnerClass$Visibility.class\n"
+ + " testDir/test/pkg/TestClass$StaticInnerClass.class\n"
+ + " testDir/test/pkg/TestClass.class\n"
+ + " testDir/test/pkg/TestClass.java\n",
+ getDirectoryContents(dir));
+
+ // Trap System.exit calls:
+ System.setSecurityManager(new SecurityManager() {
+ @Override
+ public void checkPermission(Permission perm) {
+ }
+ @Override
+ public void checkPermission(Permission perm, Object context) {
+ }
+ @Override
+ public void checkExit(int status) {
+ throw new ExitException(status);
+ }
+ });
+ try {
+ RmTypeDefs.main(new String[]{"--verbose", dir.getPath()});
+ } catch (ExitException e) {
+ assertEquals(0, e.getStatus());
+ }
+ System.setSecurityManager(null);
+
+ // TODO: check that the classes are identical
+ // BEFORE removal
+
+ assertEquals(""
+ + "testDir/\n"
+ + " testDir/android/\n"
+ + " testDir/android/annotation/\n"
+ + " testDir/android/annotation/IntDef.class\n"
+ + " testDir/android/annotation/IntDef.java\n"
+ + " testDir/test/\n"
+ + " testDir/test/pkg/\n"
+ + " testDir/test/pkg/TestClass$Inherits.class\n"
+ + " testDir/test/pkg/TestClass$StaticInnerClass.class\n"
+ + " testDir/test/pkg/TestClass.class\n"
+ + " testDir/test/pkg/TestClass.java\n",
+ getDirectoryContents(dir));
+
+
+ deleteDir(dir);
+ }
+
+ String getDirectoryContents(File root) {
+ StringBuilder sb = new StringBuilder();
+ list(sb, root, "", 0, "testDir");
+ return sb.toString();
+ }
+
+ private void list(StringBuilder sb, File file, String prefix, int depth, String rootName) {
+ for (int i = 0; i < depth; i++) {
+ sb.append(" ");
+ }
+
+ if (!prefix.isEmpty()) {
+ sb.append(prefix);
+ }
+ String fileName = file.getName();
+ if (depth == 0 && rootName != null) { // avoid temp-name
+ fileName = rootName;
+ }
+ sb.append(fileName);
+ if (file.isDirectory()) {
+ sb.append('/');
+ sb.append('\n');
+ File[] files = file.listFiles();
+ if (files != null) {
+ List children = Lists.newArrayList();
+ Collections.addAll(children, files);
+ Collections.sort(children, new Comparator() {
+ @Override
+ public int compare(File o1, File o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ });
+ prefix = prefix + fileName + "/";
+ for (File child : children) {
+ list(sb, child, prefix, depth + 1, rootName);
+ }
+ }
+ } else {
+ sb.append('\n');
+ }
+ }
+
+ /**
+ * Recursive delete directory. Mostly for fake SDKs.
+ *
+ * @param root directory to delete
+ */
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private static void deleteDir(File root) {
+ if (root.exists()) {
+ File[] files = root.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteDir(file);
+ } else {
+ file.delete();
+ }
+ }
+ }
+ root.delete();
+ }
+ }
+
+ private static class ExitException extends SecurityException {
+ private static final long serialVersionUID = 1L;
+
+ private final int mStatus;
+
+ public ExitException(int status) {
+ super("Unit test");
+ mStatus = status;
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+ }
+}
\ No newline at end of file