blob: 782ae4e4d8fce08dc5796b85b9e0560f820e5ae5 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.processor.compat.unsupportedappusage;
import static javax.tools.Diagnostic.Kind.ERROR;
import static javax.tools.StandardLocation.CLASS_OUTPUT;
import android.processor.compat.SingleAnnotationProcessor;
import android.processor.compat.SourcePosition;
import com.google.common.base.Joiner;
import com.google.common.collect.Table;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;
/**
* Annotation processor for {@code UnsupportedAppUsage} annotation.
*
* <p>This processor generates a CSV file with a mapping of dex signatures of elements annotated
* with @UnsupportedAppUsage to corresponding source positions for their UnsupportedAppUsage
* annotation.
*/
@SupportedAnnotationTypes({"android.compat.annotation.UnsupportedAppUsage"})
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public final class UnsupportedAppUsageProcessor extends SingleAnnotationProcessor {
private static final String GENERATED_INDEX_FILE_EXTENSION = ".uau";
private static final String OVERRIDE_SOURCE_POSITION_PROPERTY = "overrideSourcePosition";
private static final Pattern OVERRIDE_SOURCE_POSITION_PROPERTY_PATTERN = Pattern.compile(
"^[^:]+:\\d+:\\d+:\\d+:\\d+$");
/**
* CSV header line for the columns returned by {@link #getAnnotationIndex(String, TypeElement,
* Element)}.
*/
private static final String CSV_HEADER = Joiner.on(',').join(
"signature",
"file",
"startline",
"startcol",
"endline",
"endcol",
"properties"
);
@Override
protected void process(TypeElement annotation,
Table<PackageElement, String, List<Element>> annotatedElements) {
SignatureConverter signatureConverter = new SignatureConverter(messager);
for (PackageElement packageElement : annotatedElements.rowKeySet()) {
Map<String, List<Element>> row = annotatedElements.row(packageElement);
for (String enclosingElementName : row.keySet()) {
List<String> content = new ArrayList<>();
for (Element annotatedElement : row.get(enclosingElementName)) {
String signature = signatureConverter.getSignature(
types, annotation, annotatedElement);
if (signature != null) {
String annotationIndex = getAnnotationIndex(signature, annotation,
annotatedElement);
if (annotationIndex != null) {
content.add(annotationIndex);
}
}
}
if (content.isEmpty()) {
continue;
}
try {
FileObject resource = processingEnv.getFiler().createResource(
CLASS_OUTPUT,
packageElement.toString(),
enclosingElementName + GENERATED_INDEX_FILE_EXTENSION);
try (PrintStream outputStream = new PrintStream(resource.openOutputStream())) {
outputStream.println(CSV_HEADER);
content.forEach(outputStream::println);
}
} catch (IOException exception) {
messager.printMessage(ERROR, "Could not write CSV file: " + exception);
}
}
}
}
@Override
protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
// Implicit member refers to member not present in code, ignore.
return hasElement(mirror, "implicitMember");
}
/**
* Maps an annotated element to the source position of the @UnsupportedAppUsage annotation
* attached to it.
*
* <p>It returns CSV in the format:
* dex-signature,filename,start-line,start-col,end-line,end-col,properties
*
* <p>The positions refer to the annotation itself, *not* the annotated member. This can
* therefore be used to read just the annotation from the file, and to perform in-place
* edits on it.
*
* @return A single line of CSV text
*/
@Nullable
private String getAnnotationIndex(String signature, TypeElement annotation, Element element) {
AnnotationMirror annotationMirror = getSupportedAnnotationMirror(annotation, element);
String position = getSourcePositionOverride(element, annotationMirror);
if (position == null) {
SourcePosition sourcePosition = getSourcePosition(element, annotationMirror);
if (sourcePosition == null) {
return null;
}
position = Joiner.on(",").join(
sourcePosition.getFilename(),
sourcePosition.getStartLineNumber(),
sourcePosition.getStartColumnNumber(),
sourcePosition.getEndLineNumber(),
sourcePosition.getEndColumnNumber());
}
return Joiner.on(",").join(
signature,
position,
getAllProperties(annotationMirror));
}
@Nullable
private String getSourcePositionOverride(Element element, AnnotationMirror annotation) {
AnnotationValue annotationValue =
getAnnotationValue(element, annotation, OVERRIDE_SOURCE_POSITION_PROPERTY);
if (annotationValue == null) {
return null;
}
String parameterValue = annotationValue.getValue().toString();
if (!OVERRIDE_SOURCE_POSITION_PROPERTY_PATTERN.matcher(parameterValue).matches()) {
messager.printMessage(ERROR, String.format(
"Expected %s to have format string:int:int:int:int",
OVERRIDE_SOURCE_POSITION_PROPERTY), element, annotation);
return null;
}
return parameterValue.replace(':', ',');
}
private boolean hasElement(AnnotationMirror annotation, String elementName) {
return annotation.getElementValues().keySet().stream().anyMatch(
key -> elementName.equals(key.getSimpleName().toString()));
}
private String getAllProperties(AnnotationMirror annotation) {
return annotation.getElementValues().keySet().stream()
.filter(key -> !key.getSimpleName().toString().equals(
OVERRIDE_SOURCE_POSITION_PROPERTY))
.map(key -> String.format(
"%s=%s",
key.getSimpleName(),
getAnnotationElementValue(annotation, key)))
.collect(Collectors.joining("&"));
}
private String getAnnotationElementValue(AnnotationMirror annotation,
ExecutableElement element) {
try {
return URLEncoder.encode(annotation.getElementValues().get(element).toString(),
"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}