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