Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d7dd4ad0 authored by satayev's avatar satayev Committed by Gerrit Code Review
Browse files

Merge "Move UnsupportedAppUsageProcessor to tools/platform-compat"

parents 63d5f124 38deb176
Loading
Loading
Loading
Loading
+0 −34
Original line number Diff line number Diff line

java_library_host {
    name: "unsupportedappusage-annotation-processor-lib",
    srcs: [
        "src/**/*.java",
    ],
    static_libs: [
        "guava",
        "unsupportedappusage-annotation"
    ],
    openjdk9: {
        javacflags: [
            "--add-modules=jdk.compiler",
            "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
            "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
            "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
            "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
        ],
    },
}

java_plugin {
    name: "unsupportedappusage-annotation-processor",
    processor_class: "android.processor.unsupportedappusage.UnsupportedAppUsageProcessor",

    java_resources: [
        "META-INF/**/*",
    ],
    static_libs: [
        "unsupportedappusage-annotation-processor-lib"
    ],

    use_tools_jar: true,
}
+0 −1
Original line number Diff line number Diff line
android.processor.unsupportedappusage.UnsupportedAppUsageProcessor
+0 −258
Original line number Diff line number Diff line
/*
 * 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 com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.sun.tools.javac.code.Type;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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)) {
            switch (enclosing.getKind()) {
                case MODULE:
                    // ignore this.
                    break;
                case PACKAGE:
                    sb.append(((PackageElement) enclosing)
                            .getQualifiedName()
                            .toString()
                            .replace('.', '/'));
                    sb.append('/');
                    break;
                default:
                    sb.append(enclosing.getSimpleName()).append('$');
                    break;
            }

        }
        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();
    }

    /**
     * Creates the signature for an annotated element.
     *
     * @param annotationType type of annotation being processed.
     * @param element        element for which we want to create a signature.
     */
    public String buildSignature(Class<? extends Annotation> annotationType, Element element) {
        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;
            }
            // Obtain annotation objects
            Annotation annotation = element.getAnnotation(annotationType);
            if (annotation == null) {
                throw new IllegalStateException(
                        "Element doesn't have any UnsupportedAppUsage annotation");
            }
            try {
                Method expectedSignatureMethod = annotationType.getMethod("expectedSignature");
                // If we have an expected signature on the annotation, warn if it doesn't match.
                String expectedSignature = expectedSignatureMethod.invoke(annotation).toString();
                if (!Strings.isNullOrEmpty(expectedSignature)) {
                    if (!signature.equals(expectedSignature)) {
                        mMessager.printMessage(
                                WARNING,
                                String.format(
                                        "Expected signature doesn't match generated signature.\n"
                                                + " Expected:  %s\n Generated: %s",
                                        expectedSignature, signature),
                                element);
                    }
                }
                return signature;
            } catch (NoSuchMethodException e) {
                throw new IllegalStateException(
                        "Annotation type does not have expectedSignature parameter", e);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new IllegalStateException(
                        "Could not get expectedSignature parameter for annotation", e);
            }
        } catch (SignatureBuilderException problem) {
            problem.report(element);
            return null;
        }
    }
}
+0 −36
Original line number Diff line number Diff line
/*
 * 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.unsupportedappusage;

/**
 * Represents a source position within a source file
 */
public class SourcePosition {
    public final String filename;
    public final int startLine;
    public final int startCol;
    public final int endLine;
    public final int endCol;

    public SourcePosition(String filename, int startLine, int startCol, int endLine, int endCol) {
        this.filename = filename;
        this.startLine = startLine;
        this.startCol = startCol;
        this.endLine = endLine;
        this.endCol = endCol;
    }
}
+0 −246
Original line number Diff line number Diff line
/*
 * 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 com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
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.lang.annotation.Annotation;
import java.net.URLEncoder;
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.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

/**
 * Annotation processor for {@link UnsupportedAppUsage} annotations.
 *
 * This processor currently outputs a CSV file with a mapping of dex signatures to corresponding
 * source positions.
 *
 * This is used for automating updates to the annotations themselves.
 */
@SupportedAnnotationTypes({"android.annotation.UnsupportedAppUsage",
        "dalvik.annotation.compat.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";

    private static final ImmutableSet<Class<? extends Annotation>> SUPPORTED_ANNOTATIONS =
            ImmutableSet.of(android.annotation.UnsupportedAppUsage.class,
                    dalvik.annotation.compat.UnsupportedAppUsage.class);
    private static final ImmutableSet<String> SUPPORTED_ANNOTATION_NAMES =
            SUPPORTED_ANNOTATIONS.stream().map(annotation -> annotation.getCanonicalName()).collect(
                    ImmutableSet.toImmutableSet());
    private static final String OVERRIDE_SOURCE_POSITION_PROPERTY = "overrideSourcePosition";

    @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 (SUPPORTED_ANNOTATION_NAMES.contains(type.getQualifiedName().toString())) {
                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",
                "properties"
        );
    }

    private String encodeAnnotationProperties(AnnotationMirror annotation) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e
                : annotation.getElementValues().entrySet()) {
            if (e.getKey().getSimpleName().toString().equals(OVERRIDE_SOURCE_POSITION_PROPERTY)) {
                continue;
            }
            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(e.getKey().getSimpleName())
                    .append("=")
                    .append(URLEncoder.encode(e.getValue().toString()));
        }
        return sb.toString();
    }

    private SourcePosition getSourcePositionOverride(
            Element annotatedElement, AnnotationMirror annotation) {
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e
                : annotation.getElementValues().entrySet()) {
            if (e.getKey().getSimpleName().toString().equals(OVERRIDE_SOURCE_POSITION_PROPERTY)) {
                String[] position = e.getValue().getValue().toString().split(":");
                if (position.length != 5) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(
                            "Expected %s to have format file:startLine:startCol:endLine:endCol",
                            OVERRIDE_SOURCE_POSITION_PROPERTY), annotatedElement, annotation);
                    return null;
                }
                try {
                    return new SourcePosition(position[0], Integer.parseInt(position[1]),
                            Integer.parseInt(position[2]), Integer.parseInt(position[3]),
                            Integer.parseInt(position[4]));
                } catch (NumberFormatException nfe) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(
                            "Expected %s to have format file:startLine:startCol:endLine:endCol; "
                            + "error parsing integer: %s", OVERRIDE_SOURCE_POSITION_PROPERTY,
                            nfe.getMessage()), annotatedElement, annotation);
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * 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);
        SourcePosition position = getSourcePositionOverride(annotatedElement, unsupportedAppUsage);
        if (position == null) {
            Pair<JCTree, JCTree.JCCompilationUnit> pair =
                    javacElem.getTreeAndTopLevel(annotatedElement, unsupportedAppUsage, null);
            Position.LineMap lines = pair.snd.lineMap;
            position = new SourcePosition(
                    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)));
        }
        return Joiner.on(",").join(
                signature,
                position.filename,
                position.startLine,
                position.startCol,
                position.endLine,
                position.endCol,
                encodeAnnotationProperties(unsupportedAppUsage));
    }

    /**
     * This is the main entry point in the processor, called by the compiler.
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, Element> signatureMap = new TreeMap<>();
        SignatureBuilder sb = new SignatureBuilder(processingEnv.getMessager());
        for (Class<? extends Annotation> supportedAnnotation : SUPPORTED_ANNOTATIONS) {
            Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith(
                    supportedAnnotation);
            if (annotated.size() == 0) {
                continue;
            }
            // Build signatures for each annotated member and put them in a map from signature to
            // member.
            for (Element e : annotated) {
                String sig = sb.buildSignature(supportedAnnotation, e);
                if (sig != null) {
                    signatureMap.put(sig, e);
                }
            }
        }

        if (!signatureMap.isEmpty()) {
            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;
    }
}
Loading