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