blob: 358afae6389eed84450bb14002a5e2ad7e7519d7 [file] [log] [blame]
// Copyright 2014 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.Preconditions;
import com.google.common.primitives.Booleans;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
import com.google.devtools.build.lib.syntax.BuiltinFunction.ExtraArgKind;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* This class defines utilities to process @SkylarkSignature annotations
* to configure a given field.
*/
public class SkylarkSignatureProcessor {
/**
* Extracts a {@code FunctionSignature.WithValues<Object, SkylarkType>} from an annotation
* @param name the name of the function
* @param annotation the annotation
* @param defaultValues an optional list of default values
* @param paramDoc an optional list into which to store documentation strings
* @param enforcedTypesList an optional list into which to store effective types to enforce
*/
// NB: the two arguments paramDoc and enforcedTypesList are used to "return" extra values via
// side-effects, and that's ugly
// TODO(bazel-team): use AutoValue to declare a value type to use as return value?
public static FunctionSignature.WithValues<Object, SkylarkType> getSignature(
String name, SkylarkSignature annotation,
@Nullable Iterable<Object> defaultValues,
@Nullable List<String> paramDoc, @Nullable List<SkylarkType> enforcedTypesList) {
Preconditions.checkArgument(name.equals(annotation.name()),
"%s != %s", name, annotation.name());
ArrayList<Parameter<Object, SkylarkType>> paramList = new ArrayList<>();
HashMap<String, SkylarkType> enforcedTypes =
enforcedTypesList == null ? null : new HashMap<>();
HashMap<String, String> doc = new HashMap<>();
boolean documented = annotation.documented();
if (annotation.doc().isEmpty() && documented) {
throw new RuntimeException(String.format("function %s is undocumented", name));
}
Iterator<Object> defaultValuesIterator = defaultValues == null
? null : defaultValues.iterator();
try {
boolean named = false;
for (Param param : annotation.parameters()) {
boolean mandatory = param.defaultValue() != null && param.defaultValue().isEmpty();
Object defaultValue = mandatory ? null : getDefaultValue(param, defaultValuesIterator);
if (param.named() && !param.positional() && !named) {
named = true;
@Nullable Param starParam = null;
if (!annotation.extraPositionals().name().isEmpty()) {
starParam = annotation.extraPositionals();
}
paramList.add(getParameter(name, starParam, enforcedTypes, doc, documented,
/*mandatory=*/false, /*star=*/true, /*starStar=*/false, /*defaultValue=*/null));
}
paramList.add(getParameter(name, param, enforcedTypes, doc, documented,
mandatory, /*star=*/false, /*starStar=*/false, defaultValue));
}
if (!annotation.extraPositionals().name().isEmpty() && !named) {
paramList.add(getParameter(name, annotation.extraPositionals(), enforcedTypes, doc,
documented, /*mandatory=*/false, /*star=*/true, /*starStar=*/false,
/*defaultValue=*/null));
}
if (!annotation.extraKeywords().name().isEmpty()) {
paramList.add(
getParameter(name, annotation.extraKeywords(), enforcedTypes, doc, documented,
/*mandatory=*/false, /*star=*/false, /*starStar=*/true, /*defaultValue=*/null));
}
FunctionSignature.WithValues<Object, SkylarkType> signature =
FunctionSignature.WithValues.of(paramList);
for (String paramName : signature.getSignature().getNames()) {
if (enforcedTypesList != null) {
enforcedTypesList.add(enforcedTypes.get(paramName));
}
if (paramDoc != null) {
paramDoc.add(doc.get(paramName));
}
}
return signature;
} catch (FunctionSignature.SignatureException e) {
throw new RuntimeException(String.format(
"Invalid signature while configuring BuiltinFunction %s", name), e);
}
}
/**
* Configures the parameter of this Skylark function using the annotation.
*/
// TODO(bazel-team): Maybe have the annotation be a string representing the
// python-style calling convention including default values, and have the regular Parser
// process it? (builtin function call not allowed when evaluating values, but more complex
// values are possible by referencing variables in some definition environment).
// Then the only per-parameter information needed is a documentation string.
private static Parameter<Object, SkylarkType> getParameter(
String name, Param param, Map<String, SkylarkType> enforcedTypes,
Map<String, String> paramDoc, boolean documented,
boolean mandatory, boolean star, boolean starStar, @Nullable Object defaultValue)
throws FunctionSignature.SignatureException {
@Nullable SkylarkType officialType = null;
@Nullable SkylarkType enforcedType = null;
if (star && param == null) { // pseudo-parameter to separate positional from named-only
return new Parameter.Star<>(null);
}
if (param.type() != Object.class) {
if (param.generic1() != Object.class) {
// Enforce the proper parametric type for Skylark list and set objects
officialType = SkylarkType.of(param.type(), param.generic1());
enforcedType = officialType;
} else {
officialType = SkylarkType.of(param.type());
enforcedType = officialType;
}
if (param.callbackEnabled()) {
officialType = SkylarkType.Union.of(
officialType, SkylarkType.SkylarkFunctionType.of(name, officialType));
enforcedType = SkylarkType.Union.of(
enforcedType, SkylarkType.SkylarkFunctionType.of(name, enforcedType));
}
if (param.noneable()) {
officialType = SkylarkType.Union.of(officialType, SkylarkType.NONE);
enforcedType = SkylarkType.Union.of(enforcedType, SkylarkType.NONE);
}
}
if (enforcedTypes != null) {
enforcedTypes.put(param.name(), enforcedType);
}
if (param.doc().isEmpty() && documented) {
throw new RuntimeException(
String.format("parameter %s on method %s is undocumented", param.name(), name));
}
if (paramDoc != null) {
paramDoc.put(param.name(), param.doc());
}
if (starStar) {
return new Parameter.StarStar<>(param.name(), officialType);
} else if (star) {
return new Parameter.Star<>(param.name(), officialType);
} else if (mandatory) {
return new Parameter.Mandatory<>(param.name(), officialType);
} else if (defaultValue != null && enforcedType != null) {
Preconditions.checkArgument(enforcedType.contains(defaultValue),
"In function '%s', parameter '%s' has default value %s that isn't of enforced type %s",
name, param.name(), Printer.repr(defaultValue), enforcedType);
}
return new Parameter.Optional<>(param.name(), officialType, defaultValue);
}
static Object getDefaultValue(Param param, Iterator<Object> iterator) {
if (iterator != null) {
return iterator.next();
} else if (param.defaultValue().isEmpty()) {
return Runtime.NONE;
} else {
try (Mutability mutability = Mutability.create("initialization")) {
// Note that this Skylark environment ignores command line flags.
Environment env =
Environment.builder(mutability)
.useDefaultSemantics()
.setGlobals(Environment.CONSTANTS_ONLY)
.setEventHandler(Environment.FAIL_FAST_HANDLER)
.build()
.update("unbound", Runtime.UNBOUND);
return BuildFileAST.eval(env, param.defaultValue());
} catch (Exception e) {
throw new RuntimeException(String.format(
"Exception while processing @SkylarkSignature.Param %s, default value %s",
param.name(), param.defaultValue()), e);
}
}
}
/** Extract additional signature information for BuiltinFunction-s */
public static ExtraArgKind[] getExtraArgs(SkylarkSignature annotation) {
final int numExtraArgs =
Booleans.countTrue(
annotation.useLocation(), annotation.useAst(), annotation.useEnvironment());
if (numExtraArgs == 0) {
return null;
}
final ExtraArgKind[] extraArgs = new ExtraArgKind[numExtraArgs];
int i = 0;
if (annotation.useLocation()) {
extraArgs[i++] = ExtraArgKind.LOCATION;
}
if (annotation.useAst()) {
extraArgs[i++] = ExtraArgKind.SYNTAX_TREE;
}
if (annotation.useEnvironment()) {
extraArgs[i++] = ExtraArgKind.ENVIRONMENT;
}
return extraArgs;
}
/**
* Configure all BaseFunction-s in a class from their @SkylarkSignature annotations
* @param type a class containing BuiltinFunction fields that need be configured.
* This function is typically called in a static block to initialize a class,
* i.e. a class {@code Foo} containing @SkylarkSignature annotations would end with
* {@code static { SkylarkSignatureProcessor.configureSkylarkFunctions(Foo.class); }}
*/
public static void configureSkylarkFunctions(Class<?> type) {
for (Field field : type.getDeclaredFields()) {
if (field.isAnnotationPresent(SkylarkSignature.class)) {
// The annotated fields are often private, but we need access them.
field.setAccessible(true);
SkylarkSignature annotation = field.getAnnotation(SkylarkSignature.class);
Object value = null;
try {
value = Preconditions.checkNotNull(field.get(null),
String.format(
"Error while trying to configure %s.%s: its value is null", type, field));
if (BaseFunction.class.isAssignableFrom(field.getType())) {
BaseFunction function = (BaseFunction) value;
if (!function.isConfigured()) {
function.configure(annotation);
}
Class<?> nameSpace = function.getObjectType();
if (nameSpace != null) {
Preconditions.checkState(!(function instanceof BuiltinFunction.Factory));
nameSpace = Runtime.getCanonicalRepresentation(nameSpace);
Runtime.registerFunction(nameSpace, function);
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(String.format(
"Error while trying to configure %s.%s (value %s)", type, field, value), e);
}
}
}
}
}