// 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.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.primitives.Booleans; import com.google.devtools.build.lib.skylarkinterface.Param; import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; 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 { // A cache mapping string representation of a skylark parameter default value to the object // represented by that string. For example, "None" -> Runtime.NONE. This cache is manually // maintained (instead of using, for example, a LoadingCache), as default values may sometimes // be recursively requested. private static final Cache defaultValueCache = CacheBuilder.newBuilder().build(); /** * Extracts a {@code FunctionSignature.WithValues} from a * {@link SkylarkCallable}-annotated method. * * @param name the name of the function * @param descriptor the method descriptor * @param paramDoc an optional list into which to store documentation strings * @param enforcedTypesList an optional list into which to store effective types to enforce */ public static FunctionSignature.WithValues getSignatureForCallable( String name, MethodDescriptor descriptor, @Nullable List paramDoc, @Nullable List enforcedTypesList) { SkylarkCallable annotation = descriptor.getAnnotation(); // TODO(cparsons): Validate these properties with the annotation processor instead. Preconditions.checkArgument(name.equals(annotation.name()), "%s != %s", name, annotation.name()); boolean documented = annotation.documented(); if (annotation.doc().isEmpty() && documented) { throw new RuntimeException(String.format("function %s is undocumented", name)); } return getSignatureForCallable( name, documented, annotation.parameters(), annotation.extraPositionals(), annotation.extraKeywords(), /*defaultValues=*/ null, paramDoc, enforcedTypesList); } /** * Extracts a {@code FunctionSignature.WithValues} from a * {@link SkylarkSignature} 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 getSignatureForCallable( String name, SkylarkSignature annotation, @Nullable Iterable defaultValues, @Nullable List paramDoc, @Nullable List enforcedTypesList) { Preconditions.checkArgument(name.equals(annotation.name()), "%s != %s", name, annotation.name()); boolean documented = annotation.documented(); if (annotation.doc().isEmpty() && documented) { throw new RuntimeException(String.format("function %s is undocumented", name)); } return getSignatureForCallable(name, documented, annotation.parameters(), annotation.extraPositionals(), annotation.extraKeywords(), defaultValues, paramDoc, enforcedTypesList); } private static boolean isParamNamed(Param param) { return param.named() || param.legacyNamed(); } private static FunctionSignature.WithValues getSignatureForCallable( String name, boolean documented, Param[] parameters, @Nullable Param extraPositionals, @Nullable Param extraKeywords, @Nullable Iterable defaultValues, @Nullable List paramDoc, @Nullable List enforcedTypesList) { ArrayList> paramList = new ArrayList<>(); HashMap enforcedTypes = enforcedTypesList == null ? null : new HashMap<>(); HashMap doc = new HashMap<>(); Iterator defaultValuesIterator = defaultValues == null ? null : defaultValues.iterator(); try { boolean named = false; for (Param param : parameters) { boolean mandatory = param.defaultValue() != null && param.defaultValue().isEmpty(); Object defaultValue = mandatory ? null : getDefaultValue(param, defaultValuesIterator); if (isParamNamed(param) && !param.positional() && !named) { named = true; @Nullable Param starParam = null; if (extraPositionals != null && !extraPositionals.name().isEmpty()) { starParam = 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 (extraPositionals != null && !extraPositionals.name().isEmpty() && !named) { paramList.add(getParameter(name, extraPositionals, enforcedTypes, doc, documented, /*mandatory=*/false, /*star=*/true, /*starStar=*/false, /*defaultValue=*/null)); } if (extraKeywords != null && !extraKeywords.name().isEmpty()) { paramList.add( getParameter(name, extraKeywords, enforcedTypes, doc, documented, /*mandatory=*/false, /*star=*/false, /*starStar=*/true, /*defaultValue=*/null)); } FunctionSignature.WithValues 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 getParameter( String name, Param param, Map enforcedTypes, Map 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<>(Identifier.of(param.name()), officialType); } else if (star) { return new Parameter.Star<>(Identifier.of(param.name()), officialType); } else if (mandatory) { return new Parameter.Mandatory<>(Identifier.of(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<>(Identifier.of(param.name()), officialType, defaultValue); } static Object getDefaultValue(Param param, Iterator iterator) { return getDefaultValue(param.name(), param.defaultValue(), iterator); } static Object getDefaultValue( String paramName, String paramDefaultValue, Iterator iterator) { if (iterator != null) { return iterator.next(); } else if (paramDefaultValue.isEmpty()) { return Runtime.NONE; } else { try { Object defaultValue = defaultValueCache.getIfPresent(paramDefaultValue); if (defaultValue != null) { return defaultValue; } 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); defaultValue = BuildFileAST.eval(env, paramDefaultValue); defaultValueCache.put(paramDefaultValue, defaultValue); return defaultValue; } } catch (Exception e) { throw new RuntimeException( String.format( "Exception while processing @SkylarkSignature.Param %s, default value %s", paramName, paramDefaultValue), 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; } /** * Processes all {@link SkylarkSignature}-annotated fields in a class. * *

This includes registering these fields as builtins using {@link Runtime}, and for {@link * BaseFunction} instances, calling {@link BaseFunction#configure(SkylarkSignature)}. The fields * will be picked up by reflection even if they are not public. * *

This function should be called once per class, before the builtins registry is frozen. In * practice, this is usually called from the class's own static initializer block. E.g., a class * {@code Foo} containing {@code @SkylarkSignature} annotations would end with * {@code static { SkylarkSignatureProcessor.configureSkylarkFunctions(Foo.class); }}. * *

If you see exceptions from {@link Runtime.BuiltinRegistry} here: Be sure the class's * static initializer has in fact been called before the registry was frozen. In Bazel, see * {@link com.google.devtools.build.lib.runtime.BlazeRuntime#initSkylarkBuiltinsRegistry}. */ public static void configureSkylarkFunctions(Class type) { Runtime.BuiltinRegistry builtins = Runtime.getBuiltinRegistry(); 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)); builtins.registerBuiltin(type, field.getName(), value); 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)); builtins.registerFunction(nameSpace, function); } } } catch (IllegalAccessException e) { throw new RuntimeException(String.format( "Error while trying to configure %s.%s (value %s)", type, field, value), e); } } } } }