| // Copyright 2018 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.syntax; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; | |
| import com.google.devtools.build.lib.util.SpellChecker; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import javax.annotation.Nullable; | |
| /** | |
| * A BuiltinCallable is a callable Starlark value that reflectively invokes a | |
| * SkylarkCallable-annotated method of a Java object. | |
| */ | |
| // TODO(adonovan): make this private. Most users would be content with StarlarkCallable; the rest | |
| // need only a means of querying the function's parameters. | |
| public final class BuiltinCallable implements StarlarkCallable { | |
| private final Object obj; | |
| private final String methodName; | |
| @Nullable private final MethodDescriptor desc; | |
| /** | |
| * Constructs a BuiltinCallable for a StarlarkCallable-annotated method of the given name (as seen | |
| * by Starlark, not Java). | |
| */ | |
| // TODO(adonovan): eliminate calls to this constructor from tests; use getattr instead. | |
| BuiltinCallable(Object obj, String methodName) { | |
| this(obj, methodName, /*desc=*/ null); | |
| } | |
| /** | |
| * Constructs a BuiltinCallable for a StarlarkCallable-annotated method of the given name (as seen | |
| * by Starlark, not Java). | |
| * | |
| * <p>This constructor should be used only for ephemeral BuiltinCallable values created | |
| * transiently during a call such as {@code x.f()}, when the caller has already looked up the | |
| * MethodDescriptor using the same semantics as the thread that will be used in the call. Use the | |
| * other (slower) constructor if there is any possibility that the semantics of the {@code x.f} | |
| * operation differ from those of the thread used in the call. | |
| */ | |
| BuiltinCallable(Object obj, String methodName, MethodDescriptor desc) { | |
| this.obj = obj; | |
| this.methodName = methodName; | |
| this.desc = desc; | |
| } | |
| @Override | |
| public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named) | |
| throws EvalException, InterruptedException { | |
| MethodDescriptor desc = | |
| this.desc != null ? this.desc : getMethodDescriptor(thread.getSemantics()); | |
| Preconditions.checkArgument( | |
| !desc.isStructField(), | |
| "struct field methods should be handled by DotExpression separately"); | |
| Object[] vector = getArgumentVector(thread, desc, positional, named); | |
| return desc.call( | |
| obj instanceof String ? StringModule.INSTANCE : obj, vector, thread.mutability()); | |
| } | |
| private MethodDescriptor getMethodDescriptor(StarlarkSemantics semantics) { | |
| return CallUtils.getMethod(semantics, obj.getClass(), methodName); | |
| } | |
| /** | |
| * Returns the SkylarkCallable annotation of this Starlark-callable Java method. | |
| * | |
| * @deprecated This method is intended only for docgen, and uses the default semantics. | |
| */ | |
| @Deprecated | |
| public SkylarkCallable getAnnotation() { | |
| return getMethodDescriptor(StarlarkSemantics.DEFAULT_SEMANTICS).getAnnotation(); | |
| } | |
| @Override | |
| public String getName() { | |
| return methodName; | |
| } | |
| @Override | |
| public void repr(Printer printer) { | |
| printer.append("<built-in function " + methodName + ">"); | |
| } | |
| @Override | |
| public String toString() { | |
| return methodName; | |
| } | |
| /** | |
| * Converts the arguments of a Starlark call into the argument vector for a reflective call to a | |
| * SkylarkCallable-annotated Java method. | |
| * | |
| * @param thread the Starlark thread for the call | |
| * @param loc the location of the call expression, or BUILTIN for calls from Java | |
| * @param desc descriptor for the StarlarkCallable-annotated method | |
| * @param positional a list of positional arguments | |
| * @param named a list of named arguments, as alternating Strings/Objects. May contain dups. | |
| * @return the array of arguments which may be passed to {@link MethodDescriptor#call} | |
| * @throws EvalException if the given set of arguments are invalid for the given method. For | |
| * example, if any arguments are of unexpected type, or not all mandatory parameters are | |
| * specified by the user | |
| */ | |
| private Object[] getArgumentVector( | |
| StarlarkThread thread, | |
| MethodDescriptor desc, // intentionally shadows this.desc | |
| Object[] positional, | |
| Object[] named) | |
| throws EvalException { | |
| // Overview of steps: | |
| // - allocate vector of actual arguments of correct size. | |
| // - process positional arguments, accumulating surplus ones into *args. | |
| // - process named arguments, accumulating surplus ones into **kwargs. | |
| // - set default values for missing optionals, and report missing mandatory parameters. | |
| // - set special parameters. | |
| // The static checks ensure that positional parameters appear before named, | |
| // and mandatory positionals appear before optional. | |
| // No additional memory allocation occurs in the common (success) case. | |
| // Flag-disabled parameters are skipped during argument matching, as if they do not exist. They | |
| // are instead assigned their flag-disabled values. | |
| ParamDescriptor[] parameters = desc.getParameters(); | |
| // Allocate argument vector. | |
| int n = parameters.length; | |
| if (desc.acceptsExtraArgs()) { | |
| n++; | |
| } | |
| if (desc.acceptsExtraKwargs()) { | |
| n++; | |
| } | |
| if (desc.isUseStarlarkThread()) { | |
| n++; | |
| } | |
| Object[] vector = new Object[n]; | |
| // positional arguments | |
| int paramIndex = 0; | |
| int argIndex = 0; | |
| if (obj instanceof String) { | |
| // String methods get the string as an extra argument | |
| // because their true receiver is StringModule.INSTANCE. | |
| vector[paramIndex++] = obj; | |
| } | |
| for (; argIndex < positional.length && paramIndex < parameters.length; paramIndex++) { | |
| ParamDescriptor param = parameters[paramIndex]; | |
| if (!param.isPositional()) { | |
| break; | |
| } | |
| // disabled? | |
| if (param.disabledByFlag() != null) { | |
| // Skip disabled parameter as if not present at all. | |
| // The default value will be filled in below. | |
| continue; | |
| } | |
| Object value = positional[argIndex++]; | |
| checkParamValue(param, value); | |
| vector[paramIndex] = value; | |
| } | |
| // *args | |
| Tuple<Object> varargs = null; | |
| if (desc.acceptsExtraArgs()) { | |
| varargs = Tuple.wrap(Arrays.copyOfRange(positional, argIndex, positional.length)); | |
| } else if (argIndex < positional.length) { | |
| if (argIndex == 0) { | |
| throw Starlark.errorf("%s() got unexpected positional argument", methodName); | |
| } else { | |
| throw Starlark.errorf( | |
| "%s() accepts no more than %d positional argument%s but got %d", | |
| methodName, argIndex, plural(argIndex), positional.length); | |
| } | |
| } | |
| // named arguments | |
| LinkedHashMap<String, Object> kwargs = desc.acceptsExtraKwargs() ? new LinkedHashMap<>() : null; | |
| for (int i = 0; i < named.length; i += 2) { | |
| String name = (String) named[i]; // safe | |
| Object value = named[i + 1]; | |
| // look up parameter | |
| int index = desc.getParameterIndex(name); | |
| // unknown parameter? | |
| if (index < 0) { | |
| // spill to **kwargs | |
| if (kwargs == null) { | |
| List<String> allNames = | |
| Arrays.stream(parameters) | |
| .map(ParamDescriptor::getName) | |
| .collect(ImmutableList.toImmutableList()); | |
| throw Starlark.errorf( | |
| "%s() got unexpected keyword argument '%s'%s", | |
| methodName, name, SpellChecker.didYouMean(name, allNames)); | |
| } | |
| // duplicate named argument? | |
| if (kwargs.put(name, value) != null) { | |
| throw Starlark.errorf( | |
| "%s() got multiple values for keyword argument '%s'", methodName, name); | |
| } | |
| continue; | |
| } | |
| ParamDescriptor param = parameters[index]; | |
| // positional-only param? | |
| if (!param.isNamed()) { | |
| // spill to **kwargs | |
| if (kwargs == null) { | |
| throw Starlark.errorf( | |
| "%s() got named argument for positional-only parameter '%s'", methodName, name); | |
| } | |
| // duplicate named argument? | |
| if (kwargs.put(name, value) != null) { | |
| throw Starlark.errorf( | |
| "%s() got multiple values for keyword argument '%s'", methodName, name); | |
| } | |
| continue; | |
| } | |
| // disabled? | |
| String flag = param.disabledByFlag(); | |
| if (flag != null) { | |
| // spill to **kwargs | |
| if (kwargs == null) { | |
| throw Starlark.errorf( | |
| "in call to %s(), parameter '%s' is %s", | |
| methodName, param.getName(), disabled(flag, thread.getSemantics())); | |
| } | |
| // duplicate named argument? | |
| if (kwargs.put(name, value) != null) { | |
| throw Starlark.errorf( | |
| "%s() got multiple values for keyword argument '%s'", methodName, name); | |
| } | |
| continue; | |
| } | |
| checkParamValue(param, value); | |
| // duplicate? | |
| if (vector[index] != null) { | |
| throw Starlark.errorf("%s() got multiple values for argument '%s'", methodName, name); | |
| } | |
| vector[index] = value; | |
| } | |
| // Set default values for missing parameters, | |
| // and report any that are still missing. | |
| List<String> missingPositional = null; | |
| List<String> missingNamed = null; | |
| for (int i = 0; i < parameters.length; i++) { | |
| if (vector[i] == null) { | |
| ParamDescriptor param = parameters[i]; | |
| vector[i] = param.getDefaultValue(); | |
| if (vector[i] == null) { | |
| if (param.isPositional()) { | |
| if (missingPositional == null) { | |
| missingPositional = new ArrayList<>(); | |
| } | |
| missingPositional.add(param.getName()); | |
| } else { | |
| if (missingNamed == null) { | |
| missingNamed = new ArrayList<>(); | |
| } | |
| missingNamed.add(param.getName()); | |
| } | |
| } | |
| } | |
| } | |
| if (missingPositional != null) { | |
| throw Starlark.errorf( | |
| "%s() missing %d required positional argument%s: %s", | |
| methodName, | |
| missingPositional.size(), | |
| plural(missingPositional.size()), | |
| Joiner.on(", ").join(missingPositional)); | |
| } | |
| if (missingNamed != null) { | |
| throw Starlark.errorf( | |
| "%s() missing %d required named argument%s: %s", | |
| methodName, | |
| missingNamed.size(), | |
| plural(missingNamed.size()), | |
| Joiner.on(", ").join(missingNamed)); | |
| } | |
| // special parameters | |
| int i = parameters.length; | |
| if (desc.acceptsExtraArgs()) { | |
| vector[i++] = varargs; | |
| } | |
| if (desc.acceptsExtraKwargs()) { | |
| vector[i++] = Dict.wrap(thread.mutability(), kwargs); | |
| } | |
| if (desc.isUseStarlarkThread()) { | |
| vector[i++] = thread; | |
| } | |
| return vector; | |
| } | |
| private static String plural(int n) { | |
| return n == 1 ? "" : "s"; | |
| } | |
| private void checkParamValue(ParamDescriptor param, Object value) throws EvalException { | |
| // invalid argument? | |
| SkylarkType type = param.getSkylarkType(); | |
| if (!type.contains(value)) { | |
| throw Starlark.errorf( | |
| "in call to %s(), parameter '%s' got value of type '%s', want '%s'", | |
| methodName, param.getName(), EvalUtils.getDataTypeName(value), type); | |
| } | |
| // unexpected None? | |
| if (value == Starlark.NONE && !param.isNoneable()) { | |
| throw Starlark.errorf( | |
| "in call to %s(), parameter '%s' cannot be None", methodName, param.getName()); | |
| } | |
| } | |
| // Returns a phrase meaning "disabled" appropriate to the specified flag. | |
| private static String disabled(String flag, StarlarkSemantics semantics) { | |
| // If the flag is True, it must be a deprecation flag. Otherwise it's an experimental flag. | |
| // TODO(adonovan): is that assumption sound? | |
| if (semantics.flagValue(flag)) { | |
| return String.format( | |
| "deprecated and will be removed soon. It may be temporarily re-enabled by setting" | |
| + " --%s=false", | |
| flag); | |
| } else { | |
| return String.format( | |
| "experimental and thus unavailable with the current flags. It may be enabled by setting" | |
| + " --%s", | |
| flag); | |
| } | |
| } | |
| } |