• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ASM5;
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 
main(String[] args)61     public static void main(String[] args) {
62         new RmTypeDefs().run(args);
63     }
64 
run(String[] args)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      */
checkFile(File file)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      */
checkClass(File file)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      */
usage(PrintStream out)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      * */
addTypeDef(String name, File file)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      */
rewriteOuterClasses()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(ASM5);
211             ClassVisitor classVisitor = new ClassVisitor(ASM5, 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      */
deleteAnnotationClasses()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 
TypeDefVisitor(File file)278         public TypeDefVisitor(File file) {
279             super(ASM5);
280             mFile = file;
281         }
282 
visit( int version, int access, String name, String signature, String superName, String[] interfaces)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 
visitAnnotation(String desc, boolean visible)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(ASM5) {
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