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 package android.processor.compat.unsupportedappusage;
17 
18 import static javax.tools.Diagnostic.Kind.ERROR;
19 import static javax.tools.StandardLocation.CLASS_OUTPUT;
20 
21 import android.processor.compat.SingleAnnotationProcessor;
22 import android.processor.compat.SourcePosition;
23 
24 import com.google.common.base.Joiner;
25 import com.google.common.collect.Table;
26 
27 import java.io.IOException;
28 import java.io.PrintStream;
29 import java.io.UnsupportedEncodingException;
30 import java.net.URLEncoder;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.regex.Pattern;
35 import java.util.stream.Collectors;
36 
37 import javax.annotation.Nullable;
38 import javax.annotation.processing.SupportedAnnotationTypes;
39 import javax.annotation.processing.SupportedSourceVersion;
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.PackageElement;
46 import javax.lang.model.element.TypeElement;
47 import javax.tools.FileObject;
48 
49 /**
50  * Annotation processor for {@code UnsupportedAppUsage} annotation.
51  *
52  * <p>This processor generates a CSV file with a mapping of dex signatures of elements annotated
53  * with @UnsupportedAppUsage to corresponding source positions for their UnsupportedAppUsage
54  * annotation.
55  */
56 @SupportedAnnotationTypes({"android.compat.annotation.UnsupportedAppUsage"})
57 @SupportedSourceVersion(SourceVersion.RELEASE_9)
58 public final class UnsupportedAppUsageProcessor extends SingleAnnotationProcessor {
59 
60     private static final String GENERATED_INDEX_FILE_EXTENSION = ".uau";
61 
62     private static final String OVERRIDE_SOURCE_POSITION_PROPERTY = "overrideSourcePosition";
63     private static final Pattern OVERRIDE_SOURCE_POSITION_PROPERTY_PATTERN = Pattern.compile(
64             "^[^:]+:\\d+:\\d+:\\d+:\\d+$");
65 
66     /**
67      * CSV header line for the columns returned by {@link #getAnnotationIndex(String, TypeElement,
68      * Element)}.
69      */
70     private static final String CSV_HEADER = Joiner.on(',').join(
71             "signature",
72             "file",
73             "startline",
74             "startcol",
75             "endline",
76             "endcol",
77             "properties"
78     );
79 
80     @Override
process(TypeElement annotation, Table<PackageElement, String, List<Element>> annotatedElements)81     protected void process(TypeElement annotation,
82             Table<PackageElement, String, List<Element>> annotatedElements) {
83         SignatureConverter signatureConverter = new SignatureConverter(messager);
84 
85         for (PackageElement packageElement : annotatedElements.rowKeySet()) {
86             Map<String, List<Element>> row = annotatedElements.row(packageElement);
87             for (String enclosingElementName : row.keySet()) {
88                 List<String> content = new ArrayList<>();
89                 for (Element annotatedElement : row.get(enclosingElementName)) {
90                     String signature = signatureConverter.getSignature(
91                             types, annotation, annotatedElement);
92                     if (signature != null) {
93                         String annotationIndex = getAnnotationIndex(signature, annotation,
94                                 annotatedElement);
95                         if (annotationIndex != null) {
96                             content.add(annotationIndex);
97                         }
98                     }
99                 }
100 
101                 if (content.isEmpty()) {
102                     continue;
103                 }
104 
105                 try {
106                     FileObject resource = processingEnv.getFiler().createResource(
107                             CLASS_OUTPUT,
108                             packageElement.toString(),
109                             enclosingElementName + GENERATED_INDEX_FILE_EXTENSION);
110                     try (PrintStream outputStream = new PrintStream(resource.openOutputStream())) {
111                         outputStream.println(CSV_HEADER);
112                         content.forEach(outputStream::println);
113                     }
114                 } catch (IOException exception) {
115                     messager.printMessage(ERROR, "Could not write CSV file: " + exception);
116                 }
117             }
118         }
119     }
120 
121     @Override
ignoreAnnotatedElement(Element element, AnnotationMirror mirror)122     protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
123         // Implicit member refers to member not present in code, ignore.
124         return hasElement(mirror, "implicitMember");
125     }
126 
127     /**
128      * Maps an annotated element to the source position of the @UnsupportedAppUsage annotation
129      * attached to it.
130      *
131      * <p>It returns CSV in the format:
132      * dex-signature,filename,start-line,start-col,end-line,end-col,properties
133      *
134      * <p>The positions refer to the annotation itself, *not* the annotated member. This can
135      * therefore be used to read just the annotation from the file, and to perform in-place
136      * edits on it.
137      *
138      * @return A single line of CSV text
139      */
140     @Nullable
getAnnotationIndex(String signature, TypeElement annotation, Element element)141     private String getAnnotationIndex(String signature, TypeElement annotation, Element element) {
142         AnnotationMirror annotationMirror = getSupportedAnnotationMirror(annotation, element);
143         String position = getSourcePositionOverride(element, annotationMirror);
144         if (position == null) {
145             SourcePosition sourcePosition = getSourcePosition(element, annotationMirror);
146             if (sourcePosition == null) {
147                 return null;
148             }
149             position = Joiner.on(",").join(
150                     sourcePosition.getFilename(),
151                     sourcePosition.getStartLineNumber(),
152                     sourcePosition.getStartColumnNumber(),
153                     sourcePosition.getEndLineNumber(),
154                     sourcePosition.getEndColumnNumber());
155         }
156         return Joiner.on(",").join(
157                 signature,
158                 position,
159                 getAllProperties(annotationMirror));
160     }
161 
162     @Nullable
getSourcePositionOverride(Element element, AnnotationMirror annotation)163     private String getSourcePositionOverride(Element element, AnnotationMirror annotation) {
164         AnnotationValue annotationValue =
165                 getAnnotationValue(element, annotation, OVERRIDE_SOURCE_POSITION_PROPERTY);
166         if (annotationValue == null) {
167             return null;
168         }
169 
170         String parameterValue = annotationValue.getValue().toString();
171         if (!OVERRIDE_SOURCE_POSITION_PROPERTY_PATTERN.matcher(parameterValue).matches()) {
172             messager.printMessage(ERROR, String.format(
173                     "Expected %s to have format string:int:int:int:int",
174                     OVERRIDE_SOURCE_POSITION_PROPERTY), element, annotation);
175             return null;
176         }
177 
178         return parameterValue.replace(':', ',');
179     }
180 
hasElement(AnnotationMirror annotation, String elementName)181     private boolean hasElement(AnnotationMirror annotation, String elementName) {
182         return annotation.getElementValues().keySet().stream().anyMatch(
183                 key -> elementName.equals(key.getSimpleName().toString()));
184     }
185 
getAllProperties(AnnotationMirror annotation)186     private String getAllProperties(AnnotationMirror annotation) {
187         return annotation.getElementValues().keySet().stream()
188                 .filter(key -> !key.getSimpleName().toString().equals(
189                         OVERRIDE_SOURCE_POSITION_PROPERTY))
190                 .map(key -> String.format(
191                         "%s=%s",
192                         key.getSimpleName(),
193                         getAnnotationElementValue(annotation, key)))
194                 .collect(Collectors.joining("&"));
195     }
196 
getAnnotationElementValue(AnnotationMirror annotation, ExecutableElement element)197     private String getAnnotationElementValue(AnnotationMirror annotation,
198             ExecutableElement element) {
199         try {
200             return URLEncoder.encode(annotation.getElementValues().get(element).toString(),
201                     "UTF-8");
202         } catch (UnsupportedEncodingException e) {
203             throw new RuntimeException(e);
204         }
205     }
206 
207 }
208