diff options
author | Mathew Inwood <mathewi@google.com> | 2018-06-28 14:12:10 +0100 |
---|---|---|
committer | Mathew Inwood <mathewi@google.com> | 2018-09-05 15:01:21 +0100 |
commit | bad89e5e5b171a71e42d7c738ec97a39747e4318 (patch) | |
tree | 713d2fe9e6a0a149ac86044261094a4f9d487992 /tools/processors | |
parent | 8ac363088ce16df9d50953c1aed636616513b7d8 (diff) |
Processor for @UnsupportedAppUsage annotations.
The processor outputs unsupportedappusage_index.csv, containing source
position info for every@UnsupportedAppUsage annotation processed. It is a
mapping of dex signature to the source postion of the annotation on that
signature. It is used as input for scripts which update the annotations.
We include a META-INF file which causes the compiler to automatically
pick up the annotation processor. Otherwise we would need to explicitly
specify the processor with a -processor flag to javac.
We create a new build target for just the @UnsupportedAppUsage annotation
and the @IntDef annotation (which it depends on) so that the processor can
also depend on that directly.
The processor only runs on a new build target framework-annotation-proc
so that it is not invoked as part of a regular build. This is done so
that we don't slow down peoples builds: Soong does not support annotation
processors when javac sharding is in use. This workaround can be removed
once b/77284273 is fixed.
Test: m framework-annotation-proc
Bug: 113853502
Change-Id: Ie9cd5a90ddf7a51f6035e849703fc39ad9127557
Diffstat (limited to 'tools/processors')
4 files changed, 417 insertions, 0 deletions
diff --git a/tools/processors/unsupportedappusage/Android.bp b/tools/processors/unsupportedappusage/Android.bp new file mode 100644 index 000000000000..98f3c955a2d2 --- /dev/null +++ b/tools/processors/unsupportedappusage/Android.bp @@ -0,0 +1,15 @@ + +java_library_host { + name: "unsupportedappusage-annotation-processor", + java_resources: [ + "META-INF/**/*", + ], + srcs: [ + "src/**/*.java", + ], + static_libs: [ + "guava", + "unsupportedappusage-annotation" + ], + use_tools_jar: true, +} diff --git a/tools/processors/unsupportedappusage/META-INF/services/javax.annotation.processing.Processor b/tools/processors/unsupportedappusage/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000000..4a969d319070 --- /dev/null +++ b/tools/processors/unsupportedappusage/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +android.processor.unsupportedappusage.UnsupportedAppUsageProcessor diff --git a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java new file mode 100644 index 000000000000..ef2914610f80 --- /dev/null +++ b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java @@ -0,0 +1,228 @@ +/* + * 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.unsupportedappusage; + +import static javax.lang.model.element.ElementKind.PACKAGE; +import static javax.tools.Diagnostic.Kind.ERROR; +import static javax.tools.Diagnostic.Kind.WARNING; + +import android.annotation.UnsupportedAppUsage; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.sun.tools.javac.code.Type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.processing.Messager; +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.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Builds a dex signature for a given method or field. + */ +public class SignatureBuilder { + + private static final Map<TypeKind, String> TYPE_MAP = ImmutableMap.<TypeKind, String>builder() + .put(TypeKind.BOOLEAN, "Z") + .put(TypeKind.BYTE, "B") + .put(TypeKind.CHAR, "C") + .put(TypeKind.DOUBLE, "D") + .put(TypeKind.FLOAT, "F") + .put(TypeKind.INT, "I") + .put(TypeKind.LONG, "J") + .put(TypeKind.SHORT, "S") + .put(TypeKind.VOID, "V") + .build(); + + private final Messager mMessager; + + /** + * Exception used internally when we can't build a signature. Whenever this is thrown, an error + * will also be written to the Messager. + */ + private class SignatureBuilderException extends Exception { + public SignatureBuilderException(String message) { + super(message); + } + public void report(Element offendingElement) { + mMessager.printMessage(ERROR, getMessage(), offendingElement); + } + } + + public SignatureBuilder(Messager messager) { + mMessager = messager; + } + + /** + * Returns a list of enclosing elements for the given element, with the package first, and + * excluding the element itself. + */ + private List<Element> getEnclosingElements(Element e) { + List<Element> enclosing = new ArrayList<>(); + e = e.getEnclosingElement(); // don't include the element itself. + while (e != null) { + enclosing.add(e); + e = e.getEnclosingElement(); + } + Collections.reverse(enclosing); + return enclosing; + } + + /** + * Get the dex signature for a clazz, in format "Lpackage/name/Outer$Inner;" + */ + private String getClassSignature(TypeElement clazz) { + StringBuilder sb = new StringBuilder("L"); + for (Element enclosing : getEnclosingElements(clazz)) { + if (enclosing.getKind() == PACKAGE) { + sb.append(((PackageElement) enclosing) + .getQualifiedName() + .toString() + .replace('.', '/')); + sb.append('/'); + } else { + sb.append(enclosing.getSimpleName()).append('$'); + } + + } + return sb + .append(clazz.getSimpleName()) + .append(";") + .toString(); + } + + /** + * Returns the type signature for a given type. For primitive types, a single character. + * For classes, the class signature. For arrays, a "[" preceeding the component type. + */ + private String getTypeSignature(TypeMirror type) throws SignatureBuilderException { + String sig = TYPE_MAP.get(type.getKind()); + if (sig != null) { + return sig; + } + switch (type.getKind()) { + case ARRAY: + return "[" + getTypeSignature(((ArrayType) type).getComponentType()); + case DECLARED: + Element declaring = ((DeclaredType) type).asElement(); + if (!(declaring instanceof TypeElement)) { + throw new SignatureBuilderException( + "Can't handle declared type of kind " + declaring.getKind()); + } + return getClassSignature((TypeElement) declaring); + case TYPEVAR: + Type.TypeVar typeVar = (Type.TypeVar) type; + if (typeVar.getLowerBound().getKind() != TypeKind.NULL) { + return getTypeSignature(typeVar.getLowerBound()); + } else if (typeVar.getUpperBound().getKind() != TypeKind.NULL) { + return getTypeSignature(typeVar.getUpperBound()); + } else { + throw new SignatureBuilderException("Can't handle typevar with no bound"); + } + + default: + throw new SignatureBuilderException("Can't handle type of kind " + type.getKind()); + } + } + + /** + * Get the signature for an executable, either a method or a constructor. + * + * @param name "<init>" for constructor, else the method name + * @param method The executable element in question. + */ + private String getExecutableSignature(CharSequence name, ExecutableElement method) + throws SignatureBuilderException { + StringBuilder sig = new StringBuilder(); + sig.append(getClassSignature((TypeElement) method.getEnclosingElement())) + .append("->") + .append(name) + .append("("); + for (VariableElement param : method.getParameters()) { + sig.append(getTypeSignature(param.asType())); + } + sig.append(")") + .append(getTypeSignature(method.getReturnType())); + return sig.toString(); + } + + private String buildMethodSignature(ExecutableElement method) throws SignatureBuilderException { + return getExecutableSignature(method.getSimpleName(), method); + } + + private String buildConstructorSignature(ExecutableElement cons) + throws SignatureBuilderException { + return getExecutableSignature("<init>", cons); + } + + private String buildFieldSignature(VariableElement field) throws SignatureBuilderException { + StringBuilder sig = new StringBuilder(); + sig.append(getClassSignature((TypeElement) field.getEnclosingElement())) + .append("->") + .append(field.getSimpleName()) + .append(":") + .append(getTypeSignature(field.asType())) + ; + return sig.toString(); + } + + public String buildSignature(Element element) { + UnsupportedAppUsage uba = element.getAnnotation(UnsupportedAppUsage.class); + try { + String signature; + switch (element.getKind()) { + case METHOD: + signature = buildMethodSignature((ExecutableElement) element); + break; + case CONSTRUCTOR: + signature = buildConstructorSignature((ExecutableElement) element); + break; + case FIELD: + signature = buildFieldSignature((VariableElement) element); + break; + default: + return null; + } + // if we have an expected signature on the annotation, warn if it doesn't match. + if (!Strings.isNullOrEmpty(uba.expectedSignature())) { + if (!signature.equals(uba.expectedSignature())) { + mMessager.printMessage( + WARNING, + String.format("Expected signature doesn't match generated signature.\n" + + " Expected: %s\n Generated: %s", + uba.expectedSignature(), signature), + element); + } + } + return signature; + } catch (SignatureBuilderException problem) { + problem.report(element); + return null; + } + } +} diff --git a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java new file mode 100644 index 000000000000..1d4c435939db --- /dev/null +++ b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java @@ -0,0 +1,173 @@ +/* + * 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.unsupportedappusage; + +import static javax.tools.StandardLocation.CLASS_OUTPUT; + +import android.annotation.UnsupportedAppUsage; + +import com.google.common.base.Joiner; +import com.sun.tools.javac.model.JavacElements; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Pair; +import com.sun.tools.javac.util.Position; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Stream; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +/** + * Annotation processor for {@link UnsupportedAppUsage} annotations. + * + * This processor currently outputs two things: + * 1. A greylist.txt containing dex signatures of all annotated elements. + * 2. A CSV file with a mapping of dex signatures to corresponding source positions. + * + * The first will be used at a later stage of the build to add access flags to the dex file. The + * second is used for automating updates to the annotations themselves. + */ +@SupportedAnnotationTypes({"android.annotation.UnsupportedAppUsage"}) +public class UnsupportedAppUsageProcessor extends AbstractProcessor { + + // Package name for writing output. Output will be written to the "class output" location within + // this package. + private static final String PACKAGE = "unsupportedappusage"; + private static final String INDEX_CSV = "unsupportedappusage_index.csv"; + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + /** + * Write the contents of a stream to a text file, with one line per item. + */ + private void writeToFile(String name, + String headerLine, + Stream<?> contents) throws IOException { + PrintStream out = new PrintStream(processingEnv.getFiler().createResource( + CLASS_OUTPUT, + PACKAGE, + name) + .openOutputStream()); + out.println(headerLine); + contents.forEach(o -> out.println(o)); + if (out.checkError()) { + throw new IOException("Error when writing to " + name); + } + out.close(); + } + + /** + * Find the annotation mirror for the @UnsupportedAppUsage annotation on the given element. + */ + private AnnotationMirror getUnsupportedAppUsageAnnotationMirror(Element e) { + for (AnnotationMirror m : e.getAnnotationMirrors()) { + TypeElement type = (TypeElement) m.getAnnotationType().asElement(); + if (type.getQualifiedName().toString().equals( + UnsupportedAppUsage.class.getCanonicalName())) { + return m; + } + } + return null; + } + + /** + * Returns a CSV header line for the columns returned by + * {@link #getAnnotationIndex(String, Element)}. + */ + private String getCsvHeaders() { + return Joiner.on(',').join( + "signature", + "file", + "startline", + "startcol", + "endline", + "endcol" + ); + } + + /** + * Maps an annotated element to the source position of the @UnsupportedAppUsage annotation + * attached to it. It returns CSV in the format: + * dex-signature,filename,start-line,start-col,end-line,end-col + * + * 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. + * + * @param signature the dex signature for the element. + * @param annotatedElement The annotated element + * @return A single line of CSV text + */ + private String getAnnotationIndex(String signature, Element annotatedElement) { + JavacElements javacElem = (JavacElements) processingEnv.getElementUtils(); + AnnotationMirror unsupportedAppUsage = + getUnsupportedAppUsageAnnotationMirror(annotatedElement); + Pair<JCTree, JCTree.JCCompilationUnit> pair = + javacElem.getTreeAndTopLevel(annotatedElement, unsupportedAppUsage, null); + Position.LineMap lines = pair.snd.lineMap; + return Joiner.on(",").join( + signature, + pair.snd.getSourceFile().getName(), + lines.getLineNumber(pair.fst.pos().getStartPosition()), + lines.getColumnNumber(pair.fst.pos().getStartPosition()), + lines.getLineNumber(pair.fst.pos().getEndPosition(pair.snd.endPositions)), + lines.getColumnNumber(pair.fst.pos().getEndPosition(pair.snd.endPositions))); + } + + /** + * This is the main entry point in the processor, called by the compiler. + */ + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith( + UnsupportedAppUsage.class); + if (annotated.size() == 0) { + return true; + } + // build signatures for each annotated member, and put them in a map of signature to member + Map<String, Element> signatureMap = new TreeMap<>(); + SignatureBuilder sb = new SignatureBuilder(processingEnv.getMessager()); + for (Element e : annotated) { + String sig = sb.buildSignature(e); + if (sig != null) { + signatureMap.put(sig, e); + } + } + try { + writeToFile(INDEX_CSV, + getCsvHeaders(), + signatureMap.entrySet() + .stream() + .map(e -> getAnnotationIndex(e.getKey() ,e.getValue()))); + } catch (IOException e) { + throw new RuntimeException("Failed to write output", e); + } + return true; + } +} |