| // 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 net.starlark.java.annot; | |
| import java.lang.reflect.Method; | |
| import javax.annotation.Nullable; | |
| /** Utility functions for Starlark annotations. */ | |
| public final class StarlarkAnnotations { | |
| /** | |
| * 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 String.class and Object.class, String.class is more specific. | |
| * | |
| * <p>If either class is null, returns the other class. | |
| * | |
| * <p>If the classes are identical, returns the class. | |
| * | |
| * @throws IllegalArgumentException if neither class is assignable to the other | |
| */ | |
| private static Class<?> moreSpecific(Class<?> x, Class<?> y) { | |
| if (x == null) { | |
| return y; | |
| } else if (y == null) { | |
| return x; | |
| } else if (x.isAssignableFrom(y)) { | |
| return y; | |
| } else if (y.isAssignableFrom(x)) { | |
| 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 | |
| // @StarlarkBuiltin annotations (and are thus considered "Starlark types"). If B is not a | |
| // subclass of C (nor vice 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 @StarlarkBuiltin), 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", x, y)); | |
| } | |
| } | |
| /** | |
| * Information extracted by walking a class's ancestors' {@link StarlarkBuiltin} annotations. | |
| * | |
| * @param starlarkBuiltinAncestor the most-specified ancestor annotated with {@link | |
| * StarlarkBuiltin}. (It is guaranteed that if two ancestors both define the annotation, one | |
| * of them is a subtype of the other.) | |
| * @param assignableToStructType whether any {@code StarlarkBuiltin}-annotated ancestor has set | |
| * {@link StarlarkBuiltin#isStructType} to true. | |
| */ | |
| private record ClassInfo(Class<?> starlarkBuiltinAncestor, boolean assignableToStructType) {} | |
| // A map from a class to its ClassInfo. | |
| private static final ClassValue<ClassInfo> classInfos = | |
| new ClassValue<ClassInfo>() { | |
| @Override | |
| protected ClassInfo computeValue(Class<?> type) { | |
| return buildClassInfo(type); | |
| } | |
| }; | |
| /** | |
| * Searches a class or interface's class hierarchy for {@link StarlarkBuiltin} annotations. | |
| * | |
| * <p>If the {@link StarlarkBuiltin} annotation appears multiple times within the class hierarchy, | |
| * the returned {@link ClassInfo} will contain the annotation of the most-specified class in the | |
| * hierarchy; and {@link ClassInfo#assignableToStructType} will be true if any of the annotations | |
| * have {@code assignableToStructType=true}. | |
| * | |
| * @return a {@link ClassInfo} with {@link ClassInfo#starlarkBuiltinAncestor} set to the best-fit | |
| * class that declares a {@link StarlarkBuiltin} annotation; returns null if no class in the | |
| * hierarchy declares the annotation. | |
| * @throws IllegalArgumentException if the most-specified class in the hierarchy having the | |
| * annotation is not unique | |
| */ | |
| @Nullable | |
| private static ClassInfo buildClassInfo(Class<?> classObj) { | |
| @Nullable Class<?> bestCandidate = null; | |
| boolean assignable = false; | |
| if (classObj.isAnnotationPresent(StarlarkBuiltin.class)) { | |
| bestCandidate = classObj; | |
| assignable = classObj.getAnnotation(StarlarkBuiltin.class).isStructType(); | |
| } | |
| // Note that moreSpecific imposes a linear relationship between competing ancestors, so | |
| // `bestCandidate` and `assignable` are consistent. | |
| @Nullable Class<?> superclass = classObj.getSuperclass(); | |
| if (superclass != null) { | |
| @Nullable ClassInfo result = classInfos.get(superclass); | |
| if (result != null) { | |
| bestCandidate = moreSpecific(result.starlarkBuiltinAncestor(), bestCandidate); | |
| assignable |= result.assignableToStructType(); | |
| } | |
| } | |
| for (Class<?> interfaceObj : classObj.getInterfaces()) { | |
| @Nullable ClassInfo result = classInfos.get(interfaceObj); | |
| if (result != null) { | |
| bestCandidate = moreSpecific(result.starlarkBuiltinAncestor(), bestCandidate); | |
| assignable |= result.assignableToStructType(); | |
| } | |
| } | |
| return bestCandidate != null ? new ClassInfo(bestCandidate, assignable) : null; | |
| } | |
| /** | |
| * Returns the {@link StarlarkBuiltin} annotation for the given class or interface. If the | |
| * annotation is not found directly, its ancestor classes and interfaces are searched, and in case | |
| * multiple annotations are found, the one on the most derived class or interface is used. | |
| * | |
| * <p>Returns null if no annotation is found in the class hierarchy. | |
| */ | |
| @Nullable | |
| public static StarlarkBuiltin getStarlarkBuiltin(Class<?> classObj) { | |
| @Nullable ClassInfo classInfo = classInfos.get(classObj); | |
| return classInfo != null | |
| ? classInfo.starlarkBuiltinAncestor().getAnnotation(StarlarkBuiltin.class) | |
| : null; | |
| } | |
| /** | |
| * Returns the {@link net.starlark.java.syntax.StarlarkType} string representation provided by the | |
| * {@link StarlarkBuiltin} annotation of the given class, or null if the class doesn't have an | |
| * ancestor annotated as {@link StarlarkBuiltin}. | |
| */ | |
| @Nullable | |
| public static String typeRepr(Class<?> classObj) { | |
| @Nullable StarlarkBuiltin annotation = getStarlarkBuiltin(classObj); | |
| if (annotation == null) { | |
| return null; | |
| } | |
| String annotationTypeRepr = annotation.typeRepr(); | |
| if (!annotationTypeRepr.isEmpty()) { | |
| return annotationTypeRepr; | |
| } | |
| return annotation.name(); | |
| } | |
| /** | |
| * Returns true if the given classObj or any of its interfaces or supertypes has {@code | |
| * assignableToStructType} marked in its {@link StarlarkBuiltin} annotation. | |
| */ | |
| public static boolean isAssignableToStructType(Class<?> classObj) { | |
| @Nullable ClassInfo classInfo = classInfos.get(classObj); | |
| return classInfo != null && classInfo.assignableToStructType(); | |
| } | |
| /** | |
| * Searches {@code classObj}'s class hierarchy and returns the first superclass or interface that | |
| * is annotated with {@link StarlarkBuiltin} (including possibly {@code classObj} itself), or null | |
| * if none is found. | |
| */ | |
| @Nullable | |
| public static Class<?> getParentWithStarlarkBuiltin(Class<?> classObj) { | |
| @Nullable ClassInfo classInfo = classInfos.get(classObj); | |
| return classInfo != null ? classInfo.starlarkBuiltinAncestor() : null; | |
| } | |
| /** | |
| * Returns the {@link StarlarkMethod} 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 StarlarkMethod getStarlarkMethod(Class<?> classObj, Method method) { | |
| StarlarkMethod callable = getAnnotationOnClassMatchingSignature(classObj, method); | |
| if (callable != null) { | |
| return callable; | |
| } | |
| if (classObj.getSuperclass() != null) { | |
| StarlarkMethod annotation = getStarlarkMethod(classObj.getSuperclass(), method); | |
| if (annotation != null) { | |
| return annotation; | |
| } | |
| } | |
| for (Class<?> interfaceObj : classObj.getInterfaces()) { | |
| StarlarkMethod annotation = getStarlarkMethod(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 StarlarkMethod getStarlarkMethod(Method method) { | |
| return getStarlarkMethod(method.getDeclaringClass(), method); | |
| } | |
| /** | |
| * Returns the {@code StarlarkMethod} 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 StarlarkMethod getAnnotationOnClassMatchingSignature( | |
| Class<?> classObj, Method signatureToMatch) { | |
| // TODO(b/79877079): This method validates several invariants of @StarlarkMethod. 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(); | |
| StarlarkMethod callable = null; | |
| for (Method method : methods) { | |
| if (signatureToMatch.getName().equals(method.getName()) | |
| && method.isAnnotationPresent(StarlarkMethod.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(StarlarkMethod.class); | |
| } else { | |
| throw new IllegalStateException( | |
| String.format( | |
| "Class %s has multiple overloaded methods named '%s' annotated " | |
| + "with @StarlarkMethod", | |
| classObj, signatureToMatch.getName())); | |
| } | |
| } | |
| } | |
| return callable; | |
| } | |
| private StarlarkAnnotations() {} | |
| } |