// 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.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 com.google.devtools.build.lib.util.Preconditions; 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<String, SkylarkType>(); 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.<Object, SkylarkType>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) .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); } } } } }