Enable named arguments for SkylarkCallable annotation
This just add the support on the Skylark side, the documentation generator
still needs to be updated.
--
Change-Id: Ic26547cdb8d2c5c01839a4014c10f1b9b209b92b
Reviewed-on: https://bazel-review.googlesource.com/#/c/4247/
MOS_MIGRATED_REVID=129328278
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
index c22fcb7..6ce3e780 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
@@ -25,6 +25,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.syntax.EvalException.EvalExceptionWithJavaCause;
@@ -35,26 +36,27 @@
import com.google.devtools.build.lib.syntax.compiler.NewObject;
import com.google.devtools.build.lib.syntax.compiler.Variable.InternalVariable;
import com.google.devtools.build.lib.syntax.compiler.VariableScope;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.StringUtilities;
-
-import net.bytebuddy.description.type.TypeDescription;
-import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
-import net.bytebuddy.implementation.bytecode.Removal;
-import net.bytebuddy.implementation.bytecode.StackManipulation;
-import net.bytebuddy.implementation.bytecode.assign.TypeCasting;
-import net.bytebuddy.implementation.bytecode.constant.TextConstant;
-
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
-
import javax.annotation.Nullable;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
+import net.bytebuddy.implementation.bytecode.Removal;
+import net.bytebuddy.implementation.bytecode.StackManipulation;
+import net.bytebuddy.implementation.bytecode.assign.TypeCasting;
+import net.bytebuddy.implementation.bytecode.constant.TextConstant;
/**
* Syntax node for a function call expression.
@@ -88,41 +90,62 @@
private static final LoadingCache<Class<?>, Map<String, List<MethodDescriptor>>> methodCache =
CacheBuilder.newBuilder()
- .initialCapacity(10)
- .maximumSize(100)
- .build(new CacheLoader<Class<?>, Map<String, List<MethodDescriptor>>>() {
+ .initialCapacity(10)
+ .maximumSize(100)
+ .build(
+ new CacheLoader<Class<?>, Map<String, List<MethodDescriptor>>>() {
- @Override
- public Map<String, List<MethodDescriptor>> load(Class<?> key) throws Exception {
- Map<String, List<MethodDescriptor>> methodMap = new HashMap<>();
- for (Method method : key.getMethods()) {
- // Synthetic methods lead to false multiple matches
- if (method.isSynthetic()) {
- continue;
- }
- SkylarkCallable callable = getAnnotationFromParentClass(
- method.getDeclaringClass(), method);
- if (callable == null) {
- continue;
- }
- String name = callable.name();
- if (name.isEmpty()) {
- name = StringUtilities.toPythonStyleFunctionName(method.getName());
- }
- String signature = name + "#" + method.getParameterTypes().length;
- if (methodMap.containsKey(signature)) {
- methodMap.get(signature).add(new MethodDescriptor(method, callable));
- } else {
- methodMap.put(signature, Lists.newArrayList(new MethodDescriptor(method, callable)));
- }
- }
- return ImmutableMap.copyOf(methodMap);
- }
- });
+ @Override
+ public Map<String, List<MethodDescriptor>> load(Class<?> key) throws Exception {
+ Map<String, List<MethodDescriptor>> methodMap = new HashMap<>();
+ for (Method method : key.getMethods()) {
+ // Synthetic methods lead to false multiple matches
+ if (method.isSynthetic()) {
+ continue;
+ }
+ SkylarkCallable callable =
+ getAnnotationFromParentClass(method.getDeclaringClass(), method);
+ if (callable == null) {
+ continue;
+ }
+ Preconditions.checkArgument(
+ callable.parameters().length == 0 || !callable.structField(),
+ "Method "
+ + method
+ + " was annotated with both structField amd parameters.");
+ if (callable.parameters().length > 0 || callable.mandatoryPositionals() >= 0) {
+ int nbArgs =
+ callable.parameters().length
+ + Math.max(0, callable.mandatoryPositionals());
+ Preconditions.checkArgument(
+ nbArgs == method.getParameterTypes().length,
+ "Method "
+ + method
+ + " was annotated for "
+ + nbArgs
+ + " arguments "
+ + "but accept only "
+ + method.getParameterTypes().length
+ + " arguments.");
+ }
+ String name = callable.name();
+ if (name.isEmpty()) {
+ name = StringUtilities.toPythonStyleFunctionName(method.getName());
+ }
+ if (methodMap.containsKey(name)) {
+ methodMap.get(name).add(new MethodDescriptor(method, callable));
+ } else {
+ methodMap.put(
+ name, Lists.newArrayList(new MethodDescriptor(method, callable)));
+ }
+ }
+ return ImmutableMap.copyOf(methodMap);
+ }
+ });
/**
- * Returns a map of methods and corresponding SkylarkCallable annotations
- * of the methods of the classObj class reachable from Skylark.
+ * Returns a map of methods and corresponding SkylarkCallable annotations of the methods of the
+ * classObj class reachable from Skylark.
*/
public static ImmutableMap<Method, SkylarkCallable> collectSkylarkMethodsWithAnnotation(
Class<?> classObj) {
@@ -295,32 +318,21 @@
* Returns the list of Skylark callable Methods of objClass with the given name
* and argument number.
*/
- public static List<MethodDescriptor> getMethods(Class<?> objClass, String methodName, int argNum,
+ public static List<MethodDescriptor> getMethods(Class<?> objClass, String methodName,
Location loc) throws EvalException {
try {
- return methodCache.get(objClass).get(methodName + "#" + argNum);
+ return methodCache.get(objClass).get(methodName);
} catch (ExecutionException e) {
throw new EvalException(loc, "Method invocation failed: " + e);
}
}
/**
- * Returns the list of the Skylark name of all Skylark callable methods.
+ * Returns a set of the Skylark name of all Skylark callable methods for object of type {@code
+ * objClass}.
*/
- public static List<String> getMethodNames(Class<?> objClass)
- throws ExecutionException {
- List<String> names = new ArrayList<>();
- for (List<MethodDescriptor> methods : methodCache.get(objClass).values()) {
- for (MethodDescriptor method : methods) {
- // TODO(bazel-team): store the Skylark name in the MethodDescriptor.
- String name = method.annotation.name();
- if (name.isEmpty()) {
- name = StringUtilities.toPythonStyleFunctionName(method.method.getName());
- }
- names.add(name);
- }
- }
- return names;
+ public static Set<String> getMethodNames(Class<?> objClass) throws ExecutionException {
+ return methodCache.get(objClass).keySet();
}
static Object callMethod(MethodDescriptor methodDescriptor, String methodName, Object obj,
@@ -372,48 +384,114 @@
// TODO(bazel-team): If there's exactly one usable method, this works. If there are multiple
// matching methods, it still can be a problem. Figure out how the Java compiler does it
// exactly and copy that behaviour.
- private MethodDescriptor findJavaMethod(
- Class<?> objClass, String methodName, List<Object> args) throws EvalException {
- MethodDescriptor matchingMethod = null;
- List<MethodDescriptor> methods = getMethods(objClass, methodName, args.size(), getLocation());
+ private Pair<MethodDescriptor, List<Object>> findJavaMethod(
+ Class<?> objClass, String methodName, List<Object> args, Map<String, Object> kwargs)
+ throws EvalException {
+ Pair<MethodDescriptor, List<Object>> matchingMethod = null;
+ List<MethodDescriptor> methods = getMethods(objClass, methodName, getLocation());
if (methods != null) {
for (MethodDescriptor method : methods) {
- Class<?>[] params = method.getMethod().getParameterTypes();
- int i = 0;
- boolean matching = true;
- for (Class<?> param : params) {
- if (!param.isAssignableFrom(args.get(i).getClass())) {
- matching = false;
- break;
- }
- i++;
- }
- if (matching) {
- if (matchingMethod == null) {
- matchingMethod = method;
- } else {
- throw new EvalException(
- getLocation(),
- String.format(
- "Type %s has multiple matches for %s",
- EvalUtils.getDataTypeNameFromClass(objClass),
- formatMethod(args)));
+ if (!method.getAnnotation().structField()) {
+ List<Object> resultArgs = convertArgumentList(args, kwargs, method);
+ if (resultArgs != null) {
+ if (matchingMethod == null) {
+ matchingMethod = new Pair<>(method, resultArgs);
+ } else {
+ throw new EvalException(
+ getLocation(),
+ String.format(
+ "Type %s has multiple matches for %s",
+ EvalUtils.getDataTypeNameFromClass(objClass), formatMethod(args, kwargs)));
+ }
}
}
}
}
- if (matchingMethod != null && !matchingMethod.getAnnotation().structField()) {
- return matchingMethod;
+ if (matchingMethod == null) {
+ throw new EvalException(
+ getLocation(),
+ String.format(
+ "Type %s has no %s",
+ EvalUtils.getDataTypeNameFromClass(objClass), formatMethod(args, kwargs)));
}
- throw new EvalException(
- getLocation(),
- String.format(
- "Type %s has no %s",
- EvalUtils.getDataTypeNameFromClass(objClass),
- formatMethod(args)));
+ return matchingMethod;
}
- private String formatMethod(List<Object> args) {
+ private static SkylarkType getType(Param param) {
+ SkylarkType type =
+ param.generic1() != Object.class
+ ? SkylarkType.of(param.type(), param.generic1())
+ : SkylarkType.of(param.type());
+ return type;
+ }
+
+ /**
+ * Constructs the parameters list to actually pass to the method, filling with default values if
+ * any. If the type does not match, returns null.
+ */
+ @Nullable
+ private ImmutableList<Object> convertArgumentList(
+ List<Object> args, Map<String, Object> kwargs, MethodDescriptor method) {
+ ImmutableList.Builder<Object> builder = ImmutableList.builder();
+ Class<?>[] params = method.getMethod().getParameterTypes();
+ SkylarkCallable callable = method.getAnnotation();
+ int i = 0;
+ int nbPositional = callable.mandatoryPositionals();
+ if (nbPositional < 0) {
+ if (callable.parameters().length > 0) {
+ nbPositional = 0;
+ } else {
+ nbPositional = params.length;
+ }
+ }
+ if (nbPositional > args.size() || args.size() > nbPositional + callable.parameters().length) {
+ return null;
+ }
+ // First process the legacy positional parameters
+ for (Class<?> param : params) {
+ Object value = args.get(i);
+ if (!param.isAssignableFrom(value.getClass())) {
+ return null;
+ }
+ builder.add(value);
+ i++;
+ if (nbPositional >= 0 && i >= nbPositional) {
+ // Stops for specified parameters instead.
+ break;
+ }
+ }
+ // Then the parameters specified in callable.parameters()
+ Set<String> keys = new HashSet<>(kwargs.keySet());
+ for (Param param : callable.parameters()) {
+ SkylarkType type = getType(param);
+ if (i < args.size()) {
+ if (!param.positional() || !type.contains(args.get(i))) {
+ return null; // next positional argument is not of the good type
+ }
+ builder.add(args.get(i));
+ i++;
+ } else if (param.named() && keys.remove(param.name())) {
+ // Named parameters
+ Object value = kwargs.get(param.name());
+ if (!type.contains(value)) {
+ return null; // type does not match
+ }
+ builder.add(value);
+ } else {
+ // Use default value
+ if (param.defaultValue().isEmpty()) {
+ return null; // no default value
+ }
+ builder.add(SkylarkSignatureProcessor.getDefaultValue(param, null));
+ }
+ }
+ if (i < args.size() || !keys.isEmpty()) {
+ return null; // too many arguments
+ }
+ return builder.build();
+ }
+
+ private String formatMethod(List<Object> args, Map<String, Object> kwargs) {
StringBuilder sb = new StringBuilder();
sb.append(functionName()).append("(");
boolean first = true;
@@ -424,11 +502,20 @@
sb.append(EvalUtils.getDataTypeName(obj));
first = false;
}
+ for (Map.Entry<String, Object> kwarg : kwargs.entrySet()) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(EvalUtils.getDataTypeName(kwarg.getValue()));
+ sb.append(" ");
+ sb.append(kwarg.getKey());
+ }
return sb.append(")").toString();
}
/**
* A {@link StackManipulation} invoking addKeywordArg.
+ *
* <p>Kept close to the definition of the method to avoid reflection errors when changing it.
*/
private static final StackManipulation addKeywordArg =
@@ -615,17 +702,9 @@
obj = value;
objClass = value.getClass();
}
- if (!keyWordArgs.isEmpty()) {
- throw new EvalException(
- call.func.getLocation(),
- String.format(
- "Keyword arguments are not allowed when calling a java method"
- + "\nwhile calling method '%s' for type %s",
- method,
- EvalUtils.getDataTypeNameFromClass(objClass)));
- }
- MethodDescriptor methodDescriptor = call.findJavaMethod(objClass, method, positionalArgs);
- return callMethod(methodDescriptor, method, obj, positionalArgs.toArray(), location, env);
+ Pair<MethodDescriptor, List<Object>> javaMethod =
+ call.findJavaMethod(objClass, method, positionalArgs, keyWordArgs);
+ return callMethod(javaMethod.first, method, obj, javaMethod.second.toArray(), location, env);
}
}