Home | History | Annotate | Download | only in rmtypedefs
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tools.rmtypedefs;
     18 
     19 import com.google.common.collect.Lists;
     20 import com.google.common.collect.Sets;
     21 import com.google.common.io.Files;
     22 
     23 import org.objectweb.asm.AnnotationVisitor;
     24 import org.objectweb.asm.ClassReader;
     25 import org.objectweb.asm.ClassVisitor;
     26 import org.objectweb.asm.ClassWriter;
     27 
     28 import java.io.File;
     29 import java.io.IOException;
     30 import java.io.PrintStream;
     31 import java.util.ArrayList;
     32 import java.util.List;
     33 import java.util.Set;
     34 
     35 import static org.objectweb.asm.Opcodes.ASM4;
     36 
     37 /**
     38  * Finds and deletes typedef annotation classes (and also warns if their
     39  * retention was wrong, such that uses embeds
     40  */
     41 public class RmTypeDefs {
     42 
     43     private static final String ANNOTATION = "java/lang/annotation/Annotation";
     44     private static final String STRING_DEF = "android/annotation/StringDef";
     45     private static final String INT_DEF = "android/annotation/IntDef";
     46     private static final String INT_DEF_DESC = "L" + INT_DEF + ";";
     47     private static final String STRING_DEF_DESC = "L" + STRING_DEF + ";";
     48     private static final String RETENTION_DESC = "Ljava/lang/annotation/Retention;";
     49     private static final String RETENTION_POLICY_DESC = "Ljava/lang/annotation/RetentionPolicy;";
     50     private static final String SOURCE_RETENTION_VALUE = "SOURCE";
     51 
     52     private boolean mQuiet;
     53     private boolean mVerbose;
     54     private boolean mHaveError;
     55     private boolean mDryRun;
     56 
     57     private Set<String> mAnnotationNames = Sets.newHashSet();
     58     private List<File> mAnnotationClassFiles = Lists.newArrayList();
     59     private Set<File> mAnnotationOuterClassFiles = Sets.newHashSet();
     60 
     61     public static void main(String[] args) {
     62         new RmTypeDefs().run(args);
     63     }
     64 
     65     private void run(String[] args) {
     66         if (args.length == 0) {
     67             usage(System.err);
     68             System.exit(1);
     69         }
     70 
     71         List<File> dirs = new ArrayList<File>();
     72         for (String arg : args) {
     73             if (arg.equals("--help") || arg.equals("-h")) {
     74                 usage(System.out);
     75                 return;
     76             } else if (arg.equals("-q") || arg.equals("--quiet") || arg.equals("--silent")) {
     77                 mQuiet = true;
     78             } else if (arg.equals("-v") || arg.equals("--verbose")) {
     79                 mVerbose = true;
     80             } else if (arg.equals("-n") || arg.equals("--dry-run")) {
     81                 mDryRun = true;
     82             } else if (arg.startsWith("-")) {
     83                 System.err.println("Unknown argument " + arg);
     84                 usage(System.err);
     85                 System.exit(1);
     86 
     87             } else {
     88                 // Other arguments should be file names
     89                 File file = new File(arg);
     90                 if (file.exists()) {
     91                     dirs.add(file);
     92                 } else {
     93                     System.err.println(file + " does not exist");
     94                     usage(System.err);
     95                     System.exit(1);
     96                 }
     97             }
     98         }
     99 
    100         if (!mQuiet) {
    101             System.out.println("Deleting @IntDef and @StringDef annotation class files");
    102         }
    103 
    104         // Record typedef annotation names and files
    105         for (File dir : dirs) {
    106             checkFile(dir);
    107         }
    108 
    109         // Rewrite the .class files for any classes that *contain* typedefs as innerclasses
    110         rewriteOuterClasses();
    111 
    112         // Removes the actual .class files for the typedef annotations
    113         deleteAnnotationClasses();
    114 
    115         System.exit(mHaveError ? -1 : 0);
    116     }
    117 
    118     /**
    119      * Visits the given directory tree recursively and calls {@link #checkClass(java.io.File)}
    120      * for any .class files encountered
    121      */
    122     private void checkFile(File file) {
    123         if (file.isDirectory()) {
    124             File[] files = file.listFiles();
    125             if (files != null) {
    126                 for (File f : files) {
    127                     checkFile(f);
    128                 }
    129             }
    130         } else if (file.isFile()) {
    131             String path = file.getPath();
    132             if (path.endsWith(".class")) {
    133                 checkClass(file);
    134             } else if (path.endsWith(".jar")) {
    135                 System.err.println(path + ": Warning: Encountered .jar file; .class files "
    136                         + "are not scanned and removed inside .jar files");
    137             }
    138         }
    139     }
    140 
    141     /**
    142      * Checks the given .class file to see if it's a typedef annotation, and if so
    143      * records that fact by calling {@link #addTypeDef(String, java.io.File)}
    144      */
    145     private void checkClass(File file) {
    146         try {
    147             byte[] bytes = Files.toByteArray(file);
    148             ClassReader classReader = new ClassReader(bytes);
    149             classReader.accept(new TypeDefVisitor(file), 0);
    150         } catch (IOException e) {
    151             System.err.println("Could not read " + file + ": " + e.getLocalizedMessage());
    152             System.exit(1);
    153         }
    154     }
    155 
    156     /**
    157      * Prints usage statement.
    158      */
    159     static void usage(PrintStream out) {
    160         out.println("Android TypeDef Remover 1.0");
    161         out.println("Copyright (C) 2013 The Android Open Source Project\n");
    162         out.println("Usage: rmtypedefs folder1 [folder2 [folder3...]]\n");
    163         out.println("Options:");
    164         out.println("  -h,--help                  show this message");
    165         out.println("  -q,--quiet                 quiet");
    166         out.println("  -v,--verbose               verbose");
    167         out.println("  -n,--dry-run               dry-run only, leaves files alone");
    168         out.println("  --verify                   run extra diagnostics to verify file integrity");
    169     }
    170 
    171     /**
    172      * Records the given class name (internal name) and class file path as corresponding to a
    173      * typedef annotation
    174      * */
    175     private void addTypeDef(String name, File file) {
    176         mAnnotationClassFiles.add(file);
    177         mAnnotationNames.add(name);
    178 
    179         String fileName = file.getName();
    180         int index = fileName.lastIndexOf('$');
    181         if (index != -1) {
    182             File parentFile = file.getParentFile();
    183             assert parentFile != null : file;
    184             File container = new File(parentFile, fileName.substring(0, index) + ".class");
    185             if (container.exists()) {
    186                 mAnnotationOuterClassFiles.add(container);
    187             } else {
    188                 System.err.println("Warning: Could not find outer class " + container
    189                         + " for typedef " + file);
    190                 mHaveError = true;
    191             }
    192         }
    193     }
    194 
    195     /**
    196      * Rewrites the outer classes containing the typedefs such that they no longer refer to
    197      * the (now removed) typedef annotation inner classes
    198      */
    199     private void rewriteOuterClasses() {
    200         for (File file : mAnnotationOuterClassFiles) {
    201             byte[] bytes;
    202             try {
    203                 bytes = Files.toByteArray(file);
    204             } catch (IOException e) {
    205                 System.err.println("Could not read " + file + ": " + e.getLocalizedMessage());
    206                 mHaveError = true;
    207                 continue;
    208             }
    209 
    210             ClassWriter classWriter = new ClassWriter(ASM4);
    211             ClassVisitor classVisitor = new ClassVisitor(ASM4, classWriter) {
    212                 @Override
    213                 public void visitInnerClass(String name, String outerName, String innerName,
    214                         int access) {
    215                     if (!mAnnotationNames.contains(name)) {
    216                         super.visitInnerClass(name, outerName, innerName, access);
    217                     }
    218                 }
    219             };
    220             ClassReader reader = new ClassReader(bytes);
    221             reader.accept(classVisitor, 0);
    222             byte[] rewritten = classWriter.toByteArray();
    223             try {
    224                 Files.write(rewritten, file);
    225             } catch (IOException e) {
    226                 System.err.println("Could not write " + file + ": " + e.getLocalizedMessage());
    227                 mHaveError = true;
    228                 //noinspection UnnecessaryContinue
    229                 continue;
    230             }
    231         }
    232     }
    233 
    234     /**
    235      * Performs the actual deletion (or display, if in dry-run mode) of the typedef annotation
    236      * files
    237      */
    238     private void deleteAnnotationClasses() {
    239         for (File mFile : mAnnotationClassFiles) {
    240             if (mVerbose) {
    241                 if (mDryRun) {
    242                     System.out.println("Would delete " + mFile);
    243                 } else {
    244                     System.out.println("Deleting " + mFile);
    245                 }
    246             }
    247             if (!mDryRun) {
    248                 boolean deleted = mFile.delete();
    249                 if (!deleted) {
    250                     System.err.println("Could not delete " + mFile);
    251                     mHaveError = true;
    252                 }
    253             }
    254         }
    255     }
    256 
    257     /**
    258      * Visitor which visits .class files and checks whether each class is a typedef annotation
    259      * (and if so, calls {@link #addTypeDef(String, java.io.File)}
    260      */
    261     private class TypeDefVisitor extends ClassVisitor {
    262 
    263         /** Class file name */
    264         private File mFile;
    265 
    266         /** Class name */
    267         private String mName;
    268 
    269         /** Is this class an annotation? */
    270         private boolean mAnnotation;
    271 
    272         /** Is this annotation a typedef? Only applies if {@link #mAnnotation} */
    273         private boolean mTypedef;
    274 
    275         /** Does the annotation have source retention? Only applies if {@link #mAnnotation} */
    276         private boolean mSourceRetention;
    277 
    278         public TypeDefVisitor(File file) {
    279             super(ASM4);
    280             mFile = file;
    281         }
    282 
    283         public void visit(
    284                 int version,
    285                 int access,
    286                 String name,
    287                 String signature,
    288                 String superName,
    289                 String[] interfaces) {
    290             mName = name;
    291             mAnnotation = interfaces != null && interfaces.length >= 1
    292                     && ANNOTATION.equals(interfaces[0]);
    293 
    294             // Special case: Also delete the actual @IntDef and @StringDef .class files.
    295             // These have class file retention
    296             mTypedef = name.equals(INT_DEF) || name.equals(STRING_DEF);
    297         }
    298 
    299         public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    300             mTypedef = desc.equals(INT_DEF_DESC) || desc.equals(STRING_DEF_DESC);
    301             if (desc.equals(RETENTION_DESC)) {
    302                 return new AnnotationVisitor(ASM4) {
    303                     public void visitEnum(String name, String desc, String value) {
    304                         if (desc.equals(RETENTION_POLICY_DESC)) {
    305                             mSourceRetention = SOURCE_RETENTION_VALUE.equals(value);
    306                         }
    307                     }
    308                 };
    309             }
    310             return null;
    311         }
    312 
    313         public void visitEnd() {
    314             if (mAnnotation && mTypedef) {
    315                 if (!mSourceRetention && !mName.equals(STRING_DEF) && !mName.equals(INT_DEF)) {
    316                     System.err.println(mFile + ": Warning: Annotation should be annotated "
    317                             + "with @Retention(RetentionPolicy.SOURCE)");
    318                     mHaveError = true;
    319                 }
    320 
    321                 addTypeDef(mName, mFile);
    322             }
    323         }
    324     }
    325 }
    326 
    327