| // Copyright 2016 The Bazel Authors. All rights reserved. | |
| // | |
| // 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 com.google.devtools.build.lib.skylarkinterface; | |
| import java.lang.annotation.Annotation; | |
| import java.lang.reflect.Method; | |
| import javax.annotation.Nullable; | |
| /** | |
| * Helpers for accessing Skylark interface annotations. | |
| */ | |
| public class SkylarkInterfaceUtils { | |
| private static final class ClassWithAnnotation<T extends Annotation> { | |
| final Class<?> klass; | |
| final T annotation; | |
| ClassWithAnnotation(Class<?> klass, T annotation) { | |
| this.klass = klass; | |
| this.annotation = annotation; | |
| } | |
| } | |
| /** | |
| * Returns the more specific class of two classes. Class x is more specific than class y | |
| * if x is assignable to y. For example, of Integer.class and Object.class, Integer.class is more | |
| * specific. | |
| * | |
| * <p>If either class is null, returns the other class.</p> | |
| * | |
| * <p>If the classes are identical, returns the class.</p> | |
| * | |
| * @throws IllegalArgumentException if neither class is assignable to the other | |
| */ | |
| private static <T extends Annotation> ClassWithAnnotation<T> moreSpecificClass( | |
| ClassWithAnnotation<T> x, ClassWithAnnotation<T> y) { | |
| if (x == null) { | |
| return y; | |
| } else if (y == null) { | |
| return x; | |
| } | |
| Class<?> xClass = x.klass; | |
| Class<?> yClass = y.klass; | |
| if (xClass.isAssignableFrom(yClass)) { | |
| return y; | |
| } else if (yClass.isAssignableFrom(xClass)) { | |
| return x; | |
| } else { | |
| // If this exception occurs, it indicates the following error scenario: | |
| // | |
| // Suppose class A is a subclass of both B and C, where B and C are annotated with | |
| // @SkylarkModule annotations (and are thus considered "skylark types"). If B is not a | |
| // subclass of C (nor visa versa), then it's impossible to resolve whether A is of type | |
| // B or if A is of type C. It's both! The way to resolve this is usually to have A be its own | |
| // type (annotated with @SkylarkModule), and thus have the explicit type of A be semantically | |
| // "B and C". | |
| throw new IllegalArgumentException(String.format( | |
| "Expected one of %s and %s to be a subclass of the other", | |
| xClass, yClass)); | |
| } | |
| } | |
| /** | |
| * Searches a class or interface's class hierarchy for the given class annotation. | |
| * | |
| * <p>If the given class annotation appears multiple times within the class hierachy, this | |
| * chooses the annotation on the most-specified class in the hierarchy.</p> | |
| * | |
| * @return a {@link ClassWithAnnotation} containing the best-fit annotation and the class | |
| * it was declared on | |
| */ | |
| @Nullable | |
| private static <T extends Annotation> ClassWithAnnotation<T> searchForClassAnnotation( | |
| Class<?> classObj, | |
| Class<T> annotationClass) { | |
| if (classObj.isAnnotationPresent(annotationClass)) { | |
| return new ClassWithAnnotation<T>(classObj, classObj.getAnnotation(annotationClass)); | |
| } | |
| ClassWithAnnotation<T> bestCandidate = null; | |
| Class<?> superclass = classObj.getSuperclass(); | |
| if (superclass != null) { | |
| ClassWithAnnotation<T> result = searchForClassAnnotation(superclass, annotationClass); | |
| bestCandidate = moreSpecificClass(result, bestCandidate); | |
| } | |
| for (Class<?> interfaceObj : classObj.getInterfaces()) { | |
| ClassWithAnnotation<T> result = searchForClassAnnotation(interfaceObj, annotationClass); | |
| bestCandidate = moreSpecificClass(result, bestCandidate); | |
| } | |
| return bestCandidate; | |
| } | |
| /** | |
| * Returns the {@link SkylarkModule} annotation for the given class, if it exists, and | |
| * null otherwise. The first annotation found will be returned, starting with {@code classObj} | |
| * and following its base classes and interfaces recursively. | |
| */ | |
| @Nullable | |
| public static SkylarkModule getSkylarkModule(Class<?> classObj) { | |
| ClassWithAnnotation<SkylarkModule> result = | |
| searchForClassAnnotation(classObj, SkylarkModule.class); | |
| return result == null ? null : result.annotation; | |
| } | |
| /** | |
| * Searches {@code classObj}'s class hierarchy and returns the first superclass or interface that | |
| * is annotated with {@link SkylarkModule} (including possibly {@code classObj} itself), or null | |
| * if none is found. | |
| */ | |
| @Nullable | |
| public static Class<?> getParentWithSkylarkModule(Class<?> classObj) { | |
| ClassWithAnnotation<SkylarkModule> result = | |
| searchForClassAnnotation(classObj, SkylarkModule.class); | |
| return result == null ? null : result.klass; | |
| } | |
| /** | |
| * Searches {@code classObj}'s class hierarchy and for a superclass or interface that | |
| * is annotated with {@link SkylarkGlobalLibrary} (including possibly {@code classObj} itself), | |
| * and returns true if one is found. | |
| */ | |
| public static boolean hasSkylarkGlobalLibrary(Class<?> classObj) { | |
| ClassWithAnnotation<SkylarkGlobalLibrary> result = | |
| searchForClassAnnotation(classObj, SkylarkGlobalLibrary.class); | |
| return result != null; | |
| } | |
| /** | |
| * Returns the {@link SkylarkCallable} annotation for the given method, if it exists, and | |
| * null otherwise. | |
| * | |
| * <p>Note that the annotation may be defined on a supermethod, rather than directly on the given | |
| * method. | |
| * | |
| * <p>{@code classObj} is the class on which the given method is defined. | |
| */ | |
| @Nullable | |
| public static SkylarkCallable getSkylarkCallable(Class<?> classObj, Method method) { | |
| SkylarkCallable callable = getCallableOnClassMatchingSignature(classObj, method); | |
| if (callable != null) { | |
| return callable; | |
| } | |
| if (classObj.getSuperclass() != null) { | |
| SkylarkCallable annotation = getSkylarkCallable(classObj.getSuperclass(), method); | |
| if (annotation != null) { | |
| return annotation; | |
| } | |
| } | |
| for (Class<?> interfaceObj : classObj.getInterfaces()) { | |
| SkylarkCallable annotation = getSkylarkCallable(interfaceObj, method); | |
| if (annotation != null) { | |
| return annotation; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Convenience version of {@code getAnnotationsFromParentClass(Class, Method)} that uses | |
| * the declaring class of the method. | |
| */ | |
| @Nullable | |
| public static SkylarkCallable getSkylarkCallable(Method method) { | |
| return getSkylarkCallable(method.getDeclaringClass(), method); | |
| } | |
| /** | |
| * Returns the {@code SkylarkCallable} annotation corresponding to the given method of the given | |
| * class, or null if there is no such annotation. | |
| * | |
| * <p>This method checks assignability instead of exact matches for purposes of generics. If | |
| * Clazz has parameters BarT (extends BarInterface) and BazT (extends BazInterface), then | |
| * foo(BarT, BazT) should match if the given method signature is foo(BarImpl, BazImpl). The | |
| * signatures are in inexact match, but an "assignable" match. | |
| */ | |
| @Nullable | |
| private static SkylarkCallable getCallableOnClassMatchingSignature( | |
| Class<?> classObj, Method signatureToMatch) { | |
| // TODO(b/79877079): This method validates several invariants of @SkylarkCallable. These | |
| // invariants should be verified in annotation processor or in test, and left out of this | |
| // method. | |
| Method[] methods = classObj.getDeclaredMethods(); | |
| Class<?>[] paramsToMatch = signatureToMatch.getParameterTypes(); | |
| SkylarkCallable callable = null; | |
| for (Method method : methods) { | |
| if (signatureToMatch.getName().equals(method.getName()) | |
| && method.isAnnotationPresent(SkylarkCallable.class)) { | |
| Class<?>[] paramTypes = method.getParameterTypes(); | |
| if (paramTypes.length == paramsToMatch.length) { | |
| for (int i = 0; i < paramTypes.length; i++) { | |
| // This verifies assignability of the method signature to ensure this is not a | |
| // coincidental overload. We verify assignability instead of matching exact parameter | |
| // classes in order to match generic methods. | |
| if (!paramTypes[i].isAssignableFrom(paramsToMatch[i])) { | |
| throw new IllegalStateException( | |
| String.format( | |
| "Class %s has an incompatible overload of annotated method %s declared by %s", | |
| classObj, signatureToMatch.getName(), signatureToMatch.getDeclaringClass())); | |
| } | |
| } | |
| } | |
| if (callable == null) { | |
| callable = method.getAnnotation(SkylarkCallable.class); | |
| } else { | |
| throw new IllegalStateException( | |
| String.format( | |
| "Class %s has multiple overloaded methods named '%s' annotated " | |
| + "with @SkylarkCallable", | |
| classObj, signatureToMatch.getName())); | |
| } | |
| } | |
| } | |
| return callable; | |
| } | |
| } |