• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.compat.changeid;
18 
19 import static javax.lang.model.element.ElementKind.CLASS;
20 import static javax.lang.model.element.ElementKind.PARAMETER;
21 import static javax.tools.Diagnostic.Kind.ERROR;
22 import static javax.tools.StandardLocation.CLASS_OUTPUT;
23 
24 import android.processor.compat.SingleAnnotationProcessor;
25 import android.processor.compat.SourcePosition;
26 
27 import com.google.common.collect.ImmutableSet;
28 import com.google.common.collect.Table;
29 
30 import java.io.IOException;
31 import java.io.OutputStream;
32 import java.util.List;
33 import java.util.Objects;
34 import java.util.regex.Pattern;
35 
36 import javax.annotation.processing.SupportedAnnotationTypes;
37 import javax.annotation.processing.SupportedSourceVersion;
38 import javax.lang.model.SourceVersion;
39 import javax.lang.model.element.AnnotationMirror;
40 import javax.lang.model.element.AnnotationValue;
41 import javax.lang.model.element.Element;
42 import javax.lang.model.element.ElementKind;
43 import javax.lang.model.element.Modifier;
44 import javax.lang.model.element.PackageElement;
45 import javax.lang.model.element.TypeElement;
46 import javax.lang.model.element.VariableElement;
47 import javax.lang.model.type.TypeKind;
48 import javax.tools.FileObject;
49 
50 /**
51  * Annotation processor for ChangeId annotations.
52  *
53  * This processor outputs an XML file containing all the changeIds defined by this
54  * annotation. The file is bundled into the pratform image and used by the system server.
55  * Design doc: go/gating-and-logging.
56  */
57 @SupportedAnnotationTypes({"android.compat.annotation.ChangeId"})
58 @SupportedSourceVersion(SourceVersion.RELEASE_9)
59 public class ChangeIdProcessor extends SingleAnnotationProcessor {
60 
61     private static final String CONFIG_XML = "compat_config.xml";
62 
63     private static final String IGNORED_CLASS = "android.compat.Compatibility";
64     private static final ImmutableSet<String> IGNORED_METHOD_NAMES =
65             ImmutableSet.of("reportUnconditionalChange", "isChangeEnabled");
66 
67     private static final String CHANGE_ID_QUALIFIED_CLASS_NAME =
68             "android.compat.annotation.ChangeId";
69 
70     private static final String DISABLED_CLASS_NAME = "android.compat.annotation.Disabled";
71     private static final String ENABLED_AFTER_CLASS_NAME = "android.compat.annotation.EnabledAfter";
72     private static final String ENABLED_SINCE_CLASS_NAME = "android.compat.annotation.EnabledSince";
73     private static final String LOGGING_CLASS_NAME = "android.compat.annotation.LoggingOnly";
74     private static final String TARGET_SDK_VERSION = "targetSdkVersion";
75     private static final String OVERRIDABLE_CLASS_NAME = "android.compat.annotation.Overridable";
76 
77     private static final Pattern JAVADOC_SANITIZER = Pattern.compile("^\\s", Pattern.MULTILINE);
78     private static final Pattern HIDE_TAG_MATCHER = Pattern.compile("(\\s|^)@hide(\\s|$)");
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         for (PackageElement packageElement : annotatedElements.rowKeySet()) {
84             for (String enclosingElementName : annotatedElements.row(packageElement).keySet()) {
85                 XmlWriter writer = new XmlWriter();
86                 for (Element element : annotatedElements.get(packageElement,
87                         enclosingElementName)) {
88                     Change change =
89                             createChange(packageElement.toString(), enclosingElementName, element);
90                     writer.addChange(change);
91                 }
92 
93                 try {
94                     FileObject resource = processingEnv.getFiler().createResource(
95                             CLASS_OUTPUT, packageElement.toString(),
96                             enclosingElementName + "_" + CONFIG_XML);
97                     try (OutputStream outputStream = resource.openOutputStream()) {
98                         writer.write(outputStream);
99                     }
100                 } catch (IOException e) {
101                     messager.printMessage(ERROR, "Failed to write output: " + e);
102                 }
103             }
104         }
105     }
106 
107     @Override
ignoreAnnotatedElement(Element element, AnnotationMirror mirror)108     protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
109         // Ignore the annotations on method parameters in known methods in package android.compat
110         // (libcore/luni/src/main/java/android/compat/Compatibility.java)
111         // without generating an error.
112         if (element.getKind() == PARAMETER) {
113             Element enclosingMethod = element.getEnclosingElement();
114             Element enclosingElement = enclosingMethod.getEnclosingElement();
115             if (enclosingElement.getKind() == CLASS) {
116                 if (enclosingElement.toString().equals(IGNORED_CLASS) &&
117                         IGNORED_METHOD_NAMES.contains(enclosingMethod.getSimpleName().toString())) {
118                     return true;
119                 }
120             }
121         }
122         return !isValidChangeId(element);
123     }
124 
125     /**
126      * Checks if the provided java element is a valid change id (i.e. a long parameter with a
127      * constant value).
128      *
129      * @param element java element to check.
130      * @return true if the provided element is a legal change id that should be added to the
131      * produced XML file. If true is returned it's guaranteed that the following operations are
132      * safe.
133      */
isValidChangeId(Element element)134     private boolean isValidChangeId(Element element) {
135         if (element.getKind() != ElementKind.FIELD) {
136             messager.printMessage(
137                     ERROR,
138                     "Non FIELD element annotated with @ChangeId.",
139                     element);
140             return false;
141         }
142         if (!(element instanceof VariableElement)) {
143             messager.printMessage(
144                     ERROR,
145                     "Non variable annotated with @ChangeId.",
146                     element);
147             return false;
148         }
149         if (((VariableElement) element).getConstantValue() == null) {
150             messager.printMessage(
151                     ERROR,
152                     "Non constant/final variable annotated with @ChangeId.",
153                     element);
154             return false;
155         }
156         if (element.asType().getKind() != TypeKind.LONG) {
157             messager.printMessage(
158                     ERROR,
159                     "Variables annotated with @ChangeId must be of type long.",
160                     element);
161             return false;
162         }
163         if (!element.getModifiers().contains(Modifier.STATIC)) {
164             messager.printMessage(
165                     ERROR,
166                     "Non static variable annotated with @ChangeId.",
167                     element);
168             return false;
169         }
170        return true;
171     }
172 
createChange(String packageName, String enclosingElementName, Element element)173     private Change createChange(String packageName, String enclosingElementName, Element element) {
174         Change.Builder builder = new Change.Builder()
175                 .id((Long) ((VariableElement) element).getConstantValue())
176                 .name(element.getSimpleName().toString());
177 
178         AnnotationMirror changeId = null;
179         for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
180             final String type =
181                     ((TypeElement) mirror.getAnnotationType().asElement()).getQualifiedName()
182                             .toString();
183             final AnnotationValue sdkValue =
184                     getAnnotationValue(element, mirror, TARGET_SDK_VERSION);
185             switch (type) {
186                 case DISABLED_CLASS_NAME:
187                     builder.disabled();
188                     break;
189                 case LOGGING_CLASS_NAME:
190                     builder.loggingOnly();
191                     break;
192                 case ENABLED_AFTER_CLASS_NAME:
193                     builder.enabledAfter((Integer)(Objects.requireNonNull(sdkValue).getValue()));
194                     break;
195                 case ENABLED_SINCE_CLASS_NAME:
196                     builder.enabledSince((Integer)(Objects.requireNonNull(sdkValue).getValue()));
197                     break;
198                 case OVERRIDABLE_CLASS_NAME:
199                     builder.overridable();
200                     break;
201                 case CHANGE_ID_QUALIFIED_CLASS_NAME:
202                     changeId = mirror;
203                     break;
204             }
205         }
206 
207         String comment =
208                 elements.getDocComment(element);
209         if (comment != null) {
210             comment = HIDE_TAG_MATCHER.matcher(comment).replaceAll("");
211             comment = JAVADOC_SANITIZER.matcher(comment).replaceAll("");
212             comment = comment.replaceAll("\\n", " ");
213             builder.description(comment.trim());
214         }
215 
216         return verifyChange(element,
217                 builder.javaClass(enclosingElementName)
218                         .javaPackage(packageName)
219                         .qualifiedClass(packageName + "." + enclosingElementName)
220                         .sourcePosition(getLineNumber(element, changeId))
221                         .build());
222     }
223 
getLineNumber(Element element, AnnotationMirror mirror)224     private String getLineNumber(Element element, AnnotationMirror mirror) {
225         SourcePosition position = Objects.requireNonNull(getSourcePosition(element, mirror));
226         return String.format("%s:%d", position.getFilename(), position.getStartLineNumber());
227     }
228 
verifyChange(Element element, Change change)229     private Change verifyChange(Element element, Change change) {
230         if (change.disabled && (change.enabledAfter != null || change.enabledSince != null)) {
231             messager.printMessage(
232                     ERROR,
233                     "ChangeId cannot be annotated with both @Disabled and "
234                             + "(@EnabledAfter | @EnabledSince).",
235                     element);
236         }
237         if (change.loggingOnly && (change.disabled || change.enabledAfter != null
238                                     || change.enabledSince != null)) {
239             messager.printMessage(
240                     ERROR,
241                     "ChangeId cannot be annotated with both @LoggingOnly and "
242                             + "(@EnabledAfter | @EnabledSince | @Disabled).",
243                     element);
244         }
245         if (change.enabledAfter != null && change.enabledSince != null) {
246             messager.printMessage(
247                     ERROR,
248                     "ChangeId cannot be annotated with both @EnabledAfter and "
249                             + "@EnabledSince. Prefer using the latter.",
250                     element);
251         }
252         return change;
253     }
254 }
255