blob: 0cbf8e40aff4285bf6161ab7af1ff4c93f8394ef [file]
// 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() {}
}