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