1 /*
2  * Copyright (C) 2018 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 android.processor.unsupportedappusage;
18 
19 import static javax.tools.StandardLocation.CLASS_OUTPUT;
20 
21 import android.annotation.UnsupportedAppUsage;
22 
23 import com.google.common.base.Joiner;
24 import com.sun.tools.javac.model.JavacElements;
25 import com.sun.tools.javac.tree.JCTree;
26 import com.sun.tools.javac.util.Pair;
27 import com.sun.tools.javac.util.Position;
28 
29 import java.io.IOException;
30 import java.io.PrintStream;
31 import java.net.URLEncoder;
32 import java.util.Map;
33 import java.util.Set;
34 import java.util.TreeMap;
35 import java.util.stream.Stream;
36 
37 import javax.annotation.processing.AbstractProcessor;
38 import javax.annotation.processing.RoundEnvironment;
39 import javax.annotation.processing.SupportedAnnotationTypes;
40 import javax.lang.model.SourceVersion;
41 import javax.lang.model.element.AnnotationMirror;
42 import javax.lang.model.element.AnnotationValue;
43 import javax.lang.model.element.Element;
44 import javax.lang.model.element.ExecutableElement;
45 import javax.lang.model.element.TypeElement;
46 
47 /**
48  * Annotation processor for {@link UnsupportedAppUsage} annotations.
49  *
50  * This processor currently outputs two things:
51  * 1. A greylist.txt containing dex signatures of all annotated elements.
52  * 2. A CSV file with a mapping of dex signatures to corresponding source positions.
53  *
54  * The first will be used at a later stage of the build to add access flags to the dex file. The
55  * second is used for automating updates to the annotations themselves.
56  */
57 @SupportedAnnotationTypes({"android.annotation.UnsupportedAppUsage"})
58 public class UnsupportedAppUsageProcessor extends AbstractProcessor {
59 
60     // Package name for writing output. Output will be written to the "class output" location within
61     // this package.
62     private static final String PACKAGE = "unsupportedappusage";
63     private static final String INDEX_CSV = "unsupportedappusage_index.csv";
64 
65     @Override
getSupportedSourceVersion()66     public SourceVersion getSupportedSourceVersion() {
67         return SourceVersion.latest();
68     }
69 
70     /**
71      * Write the contents of a stream to a text file, with one line per item.
72      */
writeToFile(String name, String headerLine, Stream<?> contents)73     private void writeToFile(String name,
74             String headerLine,
75             Stream<?> contents) throws IOException {
76         PrintStream out = new PrintStream(processingEnv.getFiler().createResource(
77                 CLASS_OUTPUT,
78                 PACKAGE,
79                 name)
80                 .openOutputStream());
81         out.println(headerLine);
82         contents.forEach(o -> out.println(o));
83         if (out.checkError()) {
84             throw new IOException("Error when writing to " + name);
85         }
86         out.close();
87     }
88 
89     /**
90      * Find the annotation mirror for the @UnsupportedAppUsage annotation on the given element.
91      */
getUnsupportedAppUsageAnnotationMirror(Element e)92     private AnnotationMirror getUnsupportedAppUsageAnnotationMirror(Element e) {
93         for (AnnotationMirror m : e.getAnnotationMirrors()) {
94             TypeElement type = (TypeElement) m.getAnnotationType().asElement();
95             if (type.getQualifiedName().toString().equals(
96                     UnsupportedAppUsage.class.getCanonicalName())) {
97                 return m;
98             }
99         }
100         return null;
101     }
102 
103     /**
104      * Returns a CSV header line for the columns returned by
105      * {@link #getAnnotationIndex(String, Element)}.
106      */
getCsvHeaders()107     private String getCsvHeaders() {
108         return Joiner.on(',').join(
109                 "signature",
110                 "file",
111                 "startline",
112                 "startcol",
113                 "endline",
114                 "endcol",
115                 "properties"
116         );
117     }
118 
encodeAnnotationProperties(AnnotationMirror annotation)119     private String encodeAnnotationProperties(AnnotationMirror annotation) {
120         StringBuilder sb = new StringBuilder();
121         for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e
122                 : annotation.getElementValues().entrySet()) {
123             if (sb.length() > 0) {
124                 sb.append("&");
125             }
126             sb.append(e.getKey().getSimpleName())
127                     .append("=")
128                     .append(URLEncoder.encode(e.getValue().toString()));
129         }
130         return sb.toString();
131     }
132 
133     /**
134      * Maps an annotated element to the source position of the @UnsupportedAppUsage annotation
135      * attached to it. It returns CSV in the format:
136      *   dex-signature,filename,start-line,start-col,end-line,end-col
137      *
138      * The positions refer to the annotation itself, *not* the annotated member. This can therefore
139      * be used to read just the annotation from the file, and to perform in-place edits on it.
140      *
141      * @param signature the dex signature for the element.
142      * @param annotatedElement The annotated element
143      * @return A single line of CSV text
144      */
getAnnotationIndex(String signature, Element annotatedElement)145     private String getAnnotationIndex(String signature, Element annotatedElement) {
146         JavacElements javacElem = (JavacElements) processingEnv.getElementUtils();
147         AnnotationMirror unsupportedAppUsage =
148                 getUnsupportedAppUsageAnnotationMirror(annotatedElement);
149         Pair<JCTree, JCTree.JCCompilationUnit> pair =
150                 javacElem.getTreeAndTopLevel(annotatedElement, unsupportedAppUsage, null);
151         Position.LineMap lines = pair.snd.lineMap;
152         return Joiner.on(",").join(
153                 signature,
154                 pair.snd.getSourceFile().getName(),
155                 lines.getLineNumber(pair.fst.pos().getStartPosition()),
156                 lines.getColumnNumber(pair.fst.pos().getStartPosition()),
157                 lines.getLineNumber(pair.fst.pos().getEndPosition(pair.snd.endPositions)),
158                 lines.getColumnNumber(pair.fst.pos().getEndPosition(pair.snd.endPositions)),
159                 encodeAnnotationProperties(unsupportedAppUsage));
160     }
161 
162     /**
163      * This is the main entry point in the processor, called by the compiler.
164      */
165     @Override
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)166     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
167         Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith(
168                 UnsupportedAppUsage.class);
169         if (annotated.size() == 0) {
170             return true;
171         }
172         // build signatures for each annotated member, and put them in a map of signature to member
173         Map<String, Element> signatureMap = new TreeMap<>();
174         SignatureBuilder sb = new SignatureBuilder(processingEnv.getMessager());
175         for (Element e : annotated) {
176             String sig = sb.buildSignature(e);
177             if (sig != null) {
178                 signatureMap.put(sig, e);
179             }
180         }
181         try {
182             writeToFile(INDEX_CSV,
183                     getCsvHeaders(),
184                     signatureMap.entrySet()
185                             .stream()
186                             .map(e -> getAnnotationIndex(e.getKey() ,e.getValue())));
187         } catch (IOException e) {
188             throw new RuntimeException("Failed to write output", e);
189         }
190         return true;
191     }
192 }
193