blob: 57d4036f53c3e24b6661103d14ce2caf41f5962d [file] [log] [blame]
/*
* Copyright (C) 2019 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.changeid;
import static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.element.ElementKind.PARAMETER;
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.collect.ImmutableSet;
import com.google.common.collect.Table;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
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.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.tools.FileObject;
/**
* Annotation processor for ChangeId annotations.
*
* This processor outputs an XML file containing all the changeIds defined by this
* annotation. The file is bundled into the pratform image and used by the system server.
* Design doc: go/gating-and-logging.
*/
@SupportedAnnotationTypes({"android.compat.annotation.ChangeId"})
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class ChangeIdProcessor extends SingleAnnotationProcessor {
private static final String CONFIG_XML = "compat_config.xml";
private static final String IGNORED_CLASS = "android.compat.Compatibility";
private static final ImmutableSet<String> IGNORED_METHOD_NAMES =
ImmutableSet.of("reportUnconditionalChange", "isChangeEnabled");
private static final String CHANGE_ID_QUALIFIED_CLASS_NAME =
"android.compat.annotation.ChangeId";
private static final String DISABLED_CLASS_NAME = "android.compat.annotation.Disabled";
private static final String ENABLED_AFTER_CLASS_NAME = "android.compat.annotation.EnabledAfter";
private static final String ENABLED_SINCE_CLASS_NAME = "android.compat.annotation.EnabledSince";
private static final String LOGGING_CLASS_NAME = "android.compat.annotation.LoggingOnly";
private static final String TARGET_SDK_VERSION = "targetSdkVersion";
private static final String OVERRIDABLE_CLASS_NAME = "android.compat.annotation.Overridable";
private static final Pattern JAVADOC_SANITIZER = Pattern.compile("^\\s", Pattern.MULTILINE);
private static final Pattern HIDE_TAG_MATCHER = Pattern.compile("(\\s|^)@hide(\\s|$)");
@Override
protected void process(TypeElement annotation,
Table<PackageElement, String, List<Element>> annotatedElements) {
for (PackageElement packageElement : annotatedElements.rowKeySet()) {
for (String enclosingElementName : annotatedElements.row(packageElement).keySet()) {
XmlWriter writer = new XmlWriter();
for (Element element : annotatedElements.get(packageElement,
enclosingElementName)) {
Change change =
createChange(packageElement.toString(), enclosingElementName, element);
writer.addChange(change);
}
try {
FileObject resource = processingEnv.getFiler().createResource(
CLASS_OUTPUT, packageElement.toString(),
enclosingElementName + "_" + CONFIG_XML);
try (OutputStream outputStream = resource.openOutputStream()) {
writer.write(outputStream);
}
} catch (IOException e) {
messager.printMessage(ERROR, "Failed to write output: " + e);
}
}
}
}
@Override
protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
// Ignore the annotations on method parameters in known methods in package android.compat
// (libcore/luni/src/main/java/android/compat/Compatibility.java)
// without generating an error.
if (element.getKind() == PARAMETER) {
Element enclosingMethod = element.getEnclosingElement();
Element enclosingElement = enclosingMethod.getEnclosingElement();
if (enclosingElement.getKind() == CLASS) {
if (enclosingElement.toString().equals(IGNORED_CLASS) &&
IGNORED_METHOD_NAMES.contains(enclosingMethod.getSimpleName().toString())) {
return true;
}
}
}
return !isValidChangeId(element);
}
/**
* Checks if the provided java element is a valid change id (i.e. a long parameter with a
* constant value).
*
* @param element java element to check.
* @return true if the provided element is a legal change id that should be added to the
* produced XML file. If true is returned it's guaranteed that the following operations are
* safe.
*/
private boolean isValidChangeId(Element element) {
if (element.getKind() != ElementKind.FIELD) {
messager.printMessage(
ERROR,
"Non FIELD element annotated with @ChangeId.",
element);
return false;
}
if (!(element instanceof VariableElement)) {
messager.printMessage(
ERROR,
"Non variable annotated with @ChangeId.",
element);
return false;
}
if (((VariableElement) element).getConstantValue() == null) {
messager.printMessage(
ERROR,
"Non constant/final variable annotated with @ChangeId.",
element);
return false;
}
if (element.asType().getKind() != TypeKind.LONG) {
messager.printMessage(
ERROR,
"Variables annotated with @ChangeId must be of type long.",
element);
return false;
}
if (!element.getModifiers().contains(Modifier.STATIC)) {
messager.printMessage(
ERROR,
"Non static variable annotated with @ChangeId.",
element);
return false;
}
return true;
}
private Change createChange(String packageName, String enclosingElementName, Element element) {
Change.Builder builder = new Change.Builder()
.id((Long) ((VariableElement) element).getConstantValue())
.name(element.getSimpleName().toString());
AnnotationMirror changeId = null;
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
final String type =
((TypeElement) mirror.getAnnotationType().asElement()).getQualifiedName()
.toString();
final AnnotationValue sdkValue =
getAnnotationValue(element, mirror, TARGET_SDK_VERSION);
switch (type) {
case DISABLED_CLASS_NAME:
builder.disabled();
break;
case LOGGING_CLASS_NAME:
builder.loggingOnly();
break;
case ENABLED_AFTER_CLASS_NAME:
builder.enabledAfter((Integer)(Objects.requireNonNull(sdkValue).getValue()));
break;
case ENABLED_SINCE_CLASS_NAME:
builder.enabledSince((Integer)(Objects.requireNonNull(sdkValue).getValue()));
break;
case OVERRIDABLE_CLASS_NAME:
builder.overridable();
break;
case CHANGE_ID_QUALIFIED_CLASS_NAME:
changeId = mirror;
break;
}
}
String comment =
elements.getDocComment(element);
if (comment != null) {
comment = HIDE_TAG_MATCHER.matcher(comment).replaceAll("");
comment = JAVADOC_SANITIZER.matcher(comment).replaceAll("");
comment = comment.replaceAll("\\n", " ");
builder.description(comment.trim());
}
return verifyChange(element,
builder.javaClass(enclosingElementName)
.javaPackage(packageName)
.qualifiedClass(packageName + "." + enclosingElementName)
.sourcePosition(getLineNumber(element, changeId))
.build());
}
private String getLineNumber(Element element, AnnotationMirror mirror) {
SourcePosition position = Objects.requireNonNull(getSourcePosition(element, mirror));
return String.format("%s:%d", position.getFilename(), position.getStartLineNumber());
}
private Change verifyChange(Element element, Change change) {
if (change.disabled && (change.enabledAfter != null || change.enabledSince != null)) {
messager.printMessage(
ERROR,
"ChangeId cannot be annotated with both @Disabled and "
+ "(@EnabledAfter | @EnabledSince).",
element);
}
if (change.loggingOnly && (change.disabled || change.enabledAfter != null
|| change.enabledSince != null)) {
messager.printMessage(
ERROR,
"ChangeId cannot be annotated with both @LoggingOnly and "
+ "(@EnabledAfter | @EnabledSince | @Disabled).",
element);
}
if (change.enabledAfter != null && change.enabledSince != null) {
messager.printMessage(
ERROR,
"ChangeId cannot be annotated with both @EnabledAfter and "
+ "@EnabledSince. Prefer using the latter.",
element);
}
return change;
}
}