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("reportChange", "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 LOGGING_CLASS_NAME = "android.compat.annotation.LoggingOnly";
73     private static final String TARGET_SDK_VERSION = "targetSdkVersion";
74 
75     private static final Pattern JAVADOC_SANITIZER = Pattern.compile("^\\s", Pattern.MULTILINE);
76     private static final Pattern HIDE_TAG_MATCHER = Pattern.compile("(\\s|^)@hide(\\s|$)");
77 
78     @Override
process(TypeElement annotation, Table<PackageElement, String, List<Element>> annotatedElements)79     protected void process(TypeElement annotation,
80             Table<PackageElement, String, List<Element>> annotatedElements) {
81         for (PackageElement packageElement : annotatedElements.rowKeySet()) {
82             for (String enclosingElementName : annotatedElements.row(packageElement).keySet()) {
83                 XmlWriter writer = new XmlWriter();
84                 for (Element element : annotatedElements.get(packageElement,
85                         enclosingElementName)) {
86                     Change change =
87                             createChange(packageElement.toString(), enclosingElementName, element);
88                     writer.addChange(change);
89                 }
90 
91                 try {
92                     FileObject resource = processingEnv.getFiler().createResource(
93                             CLASS_OUTPUT, packageElement.toString(),
94                             enclosingElementName + "_" + CONFIG_XML);
95                     try (OutputStream outputStream = resource.openOutputStream()) {
96                         writer.write(outputStream);
97                     }
98                 } catch (IOException e) {
99                     messager.printMessage(ERROR, "Failed to write output: " + e);
100                 }
101             }
102         }
103     }
104 
105     @Override
ignoreAnnotatedElement(Element element, AnnotationMirror mirror)106     protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
107         // Ignore the annotations on method parameters in known methods in package android.compat
108         // (libcore/luni/src/main/java/android/compat/Compatibility.java)
109         // without generating an error.
110         if (element.getKind() == PARAMETER) {
111             Element enclosingMethod = element.getEnclosingElement();
112             Element enclosingElement = enclosingMethod.getEnclosingElement();
113             if (enclosingElement.getKind() == CLASS) {
114                 if (enclosingElement.toString().equals(IGNORED_CLASS) &&
115                         IGNORED_METHOD_NAMES.contains(enclosingMethod.getSimpleName().toString())) {
116                     return true;
117                 }
118             }
119         }
120         return !isValidChangeId(element);
121     }
122 
123     /**
124      * Checks if the provided java element is a valid change id (i.e. a long parameter with a
125      * constant value).
126      *
127      * @param element java element to check.
128      * @return true if the provided element is a legal change id that should be added to the
129      * produced XML file. If true is returned it's guaranteed that the following operations are
130      * safe.
131      */
isValidChangeId(Element element)132     private boolean isValidChangeId(Element element) {
133         if (element.getKind() != ElementKind.FIELD) {
134             messager.printMessage(
135                     ERROR,
136                     "Non FIELD element annotated with @ChangeId.",
137                     element);
138             return false;
139         }
140         if (!(element instanceof VariableElement)) {
141             messager.printMessage(
142                     ERROR,
143                     "Non variable annotated with @ChangeId.",
144                     element);
145             return false;
146         }
147         if (((VariableElement) element).getConstantValue() == null) {
148             messager.printMessage(
149                     ERROR,
150                     "Non constant/final variable annotated with @ChangeId.",
151                     element);
152             return false;
153         }
154         if (element.asType().getKind() != TypeKind.LONG) {
155             messager.printMessage(
156                     ERROR,
157                     "Variables annotated with @ChangeId must be of type long.",
158                     element);
159             return false;
160         }
161         if (!element.getModifiers().contains(Modifier.STATIC)) {
162             messager.printMessage(
163                     ERROR,
164                     "Non static variable annotated with @ChangeId.",
165                     element);
166             return false;
167         }
168         return true;
169     }
170 
createChange(String packageName, String enclosingElementName, Element element)171     private Change createChange(String packageName, String enclosingElementName, Element element) {
172         Change.Builder builder = new Change.Builder()
173                 .id((Long) ((VariableElement) element).getConstantValue())
174                 .name(element.getSimpleName().toString());
175 
176         AnnotationMirror changeId = null;
177         for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
178             String type =
179                     ((TypeElement) mirror.getAnnotationType().asElement()).getQualifiedName().toString();
180             switch (type) {
181                 case DISABLED_CLASS_NAME:
182                     builder.disabled();
183                     break;
184                 case LOGGING_CLASS_NAME:
185                     builder.loggingOnly();
186                     break;
187                 case ENABLED_AFTER_CLASS_NAME:
188                     AnnotationValue value = getAnnotationValue(element, mirror, TARGET_SDK_VERSION);
189                     builder.enabledAfter((Integer)(Objects.requireNonNull(value).getValue()));
190                     break;
191                 case CHANGE_ID_QUALIFIED_CLASS_NAME:
192                     changeId = mirror;
193                     break;
194             }
195         }
196 
197         String comment =
198                 elements.getDocComment(element);
199         if (comment != null) {
200             comment = HIDE_TAG_MATCHER.matcher(comment).replaceAll("");
201             comment = JAVADOC_SANITIZER.matcher(comment).replaceAll("");
202             comment = comment.replaceAll("\\n", " ");
203             builder.description(comment.trim());
204         }
205 
206         return verifyChange(element,
207                 builder.javaClass(enclosingElementName)
208                         .javaPackage(packageName)
209                         .qualifiedClass(packageName + "." + enclosingElementName)
210                         .sourcePosition(getLineNumber(element, changeId))
211                         .build());
212     }
213 
getLineNumber(Element element, AnnotationMirror mirror)214     private String getLineNumber(Element element, AnnotationMirror mirror) {
215         SourcePosition position = Objects.requireNonNull(getSourcePosition(element, mirror));
216         return String.format("%s:%d", position.getFilename(), position.getStartLineNumber());
217     }
218 
verifyChange(Element element, Change change)219     private Change verifyChange(Element element, Change change) {
220         if (change.disabled && change.enabledAfter != null) {
221             messager.printMessage(
222                     ERROR,
223                     "ChangeId cannot be annotated with both @Disabled and @EnabledAfter.",
224                     element);
225         }
226         if (change.loggingOnly && (change.disabled || change.enabledAfter != null)) {
227             messager.printMessage(
228                     ERROR,
229                     "ChangeId cannot be annotated with both @LoggingOnly and "
230                             + "(@EnabledAfter | @Disabled).",
231                     element);
232         }
233         return change;
234     }
235 }
236