| // 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.collect.ImmutableList; |
| import com.google.devtools.build.lib.skylarkinterface.Param; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.function.BiFunction; |
| import javax.annotation.Nullable; |
| |
| /** |
| * This class defines utilities to process @SkylarkCallable 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" -> Starlark.NONE. This cache is manually |
| // maintained (instead of using, for example, a LoadingCache), as default values may sometimes |
| // be recursively requested. |
| private static final ConcurrentHashMap<String, Object> defaultValueCache = |
| new ConcurrentHashMap<>(); |
| |
| /** Holds signature information extracted from a method's annotation. */ |
| public static final class SignatureInfo { |
| public final FunctionSignature signature; |
| @Nullable public final List<Object> defaultValues; |
| @Nullable final List<SkylarkType> types; // "official" types (may differ from "enforced") |
| |
| SignatureInfo( |
| FunctionSignature signature, |
| @Nullable List<Object> defaultValues, |
| @Nullable List<SkylarkType> types) { |
| this.signature = signature; |
| this.defaultValues = defaultValues; |
| this.types = types; |
| } |
| } |
| |
| /** Extracts signature information from a {@link SkylarkCallable}-annotated method descriptor. */ |
| public static SignatureInfo getSignatureForCallable(MethodDescriptor descriptor) { |
| SkylarkCallable annotation = descriptor.getAnnotation(); |
| |
| boolean documented = annotation.documented(); |
| if (annotation.doc().isEmpty() && documented) { |
| throw new IllegalStateException( |
| String.format("function %s is undocumented", annotation.name())); |
| } |
| |
| return getSignatureForCallableImpl( |
| annotation.name(), |
| documented, |
| annotation.parameters(), |
| annotation.extraPositionals(), |
| annotation.extraKeywords(), |
| /*paramDoc=*/ null, |
| /*enforcedTypesList=*/ null); |
| } |
| |
| private static boolean isParamNamed(Param param) { |
| return param.named() || param.legacyNamed(); |
| } |
| |
| // 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. |
| |
| // Build-time annotation processing ensures mandatory parameters do not follow optional ones. |
| private static SignatureInfo getSignatureForCallableImpl( |
| final String name, |
| final boolean documented, |
| Param[] parameters, |
| @Nullable Param extraPositionals, |
| @Nullable Param extraKeywords, |
| @Nullable List<String> paramDoc, |
| @Nullable List<SkylarkType> enforcedTypesList) { |
| final HashMap<String, SkylarkType> enforcedTypes = new HashMap<>(); |
| final HashMap<String, String> doc = new HashMap<>(); |
| |
| // TODO(adonovan): simplify this logic, possibly sharing or delegating to pieces of the |
| // analogous logic in the parser/validator. |
| |
| BiFunction<Param, Object, SkylarkType> getParameterType = |
| (Param param, Object defaultValue) -> |
| getParameterType(name, documented, enforcedTypes, doc, param, defaultValue); |
| |
| int mandatoryPositionals = 0; |
| int optionalPositionals = 0; |
| int mandatoryNamedOnly = 0; |
| int optionalNamedOnly = 0; |
| boolean hasStar = false; |
| String star = null; |
| String starStar = null; |
| SkylarkType starType = null; |
| SkylarkType starStarType = null; |
| ArrayList<String> params = new ArrayList<>(); |
| ArrayList<Object> defaults = new ArrayList<>(); |
| ArrayList<SkylarkType> types = new ArrayList<>(); |
| // optional named-only parameters are kept aside to be spliced after the mandatory ones. |
| ArrayList<String> optionalNamedOnlyParams = new ArrayList<>(); |
| ArrayList<SkylarkType> optionalNamedOnlyTypes = new ArrayList<>(); |
| ArrayList<Object> optionalNamedOnlyDefaultValues = new ArrayList<>(); |
| |
| for (Param param : parameters) { |
| // Implicit * or *args parameter separates transition from positional to named. |
| // f (..., *, ... ) or f(..., *args, ...) |
| if (isParamNamed(param) && !param.positional() && !hasStar) { |
| hasStar = true; |
| if (extraPositionals != null && !extraPositionals.name().isEmpty()) { |
| starType = getParameterType.apply(extraPositionals, null); |
| star = extraPositionals.name(); |
| } |
| } |
| |
| boolean mandatory = param.defaultValue().isEmpty(); |
| if (mandatory) { |
| // f(..., name, ...): required parameter |
| SkylarkType t = getParameterType.apply(param, null); |
| params.add(param.name()); |
| types.add(t); |
| if (hasStar) { |
| mandatoryNamedOnly++; |
| } else { |
| mandatoryPositionals++; |
| } |
| |
| } else { |
| // f(..., name=value, ...): optional parameter |
| Object defaultValue = getDefaultValue(param); |
| SkylarkType t = getParameterType.apply(param, defaultValue); |
| if (hasStar) { |
| optionalNamedOnly++; |
| optionalNamedOnlyParams.add(param.name()); |
| optionalNamedOnlyTypes.add(t); |
| optionalNamedOnlyDefaultValues.add(defaultValue); |
| } else { |
| optionalPositionals++; |
| params.add(param.name()); |
| types.add(t); |
| defaults.add(defaultValue); |
| } |
| } |
| } |
| params.addAll(optionalNamedOnlyParams); |
| types.addAll(optionalNamedOnlyTypes); |
| defaults.addAll(optionalNamedOnlyDefaultValues); |
| |
| // f(..., *args, ...) |
| if (extraPositionals != null && !extraPositionals.name().isEmpty() && !hasStar) { |
| star = extraPositionals.name(); |
| starType = getParameterType.apply(extraPositionals, null); |
| } |
| if (star != null) { |
| params.add(star); |
| types.add(starType); |
| } |
| |
| // f(..., **kwargs) |
| if (extraKeywords != null && !extraKeywords.name().isEmpty()) { |
| starStar = extraKeywords.name(); |
| starStarType = getParameterType.apply(extraKeywords, null); |
| params.add(starStar); |
| types.add(starStarType); |
| } |
| |
| FunctionSignature signature = |
| FunctionSignature.create( |
| mandatoryPositionals, |
| optionalPositionals, |
| mandatoryNamedOnly, |
| optionalNamedOnly, |
| star != null, |
| starStar != null, |
| ImmutableList.copyOf(params)); |
| |
| for (String paramName : signature.getParameterNames()) { |
| if (enforcedTypesList != null) { |
| enforcedTypesList.add(enforcedTypes.get(paramName)); |
| } |
| if (paramDoc != null) { |
| paramDoc.add(doc.get(paramName)); |
| } |
| } |
| return new SignatureInfo(signature, defaults, types); |
| } |
| |
| // getParameterType returns the parameter's type from the @Param annotation, |
| // applies other checks and populates the type and doc mappings. |
| private static SkylarkType getParameterType( |
| // Param-independent: |
| String name, |
| boolean documented, |
| HashMap<String, SkylarkType> enforcedTypes, |
| HashMap<String, String> doc, |
| // Param-specific: |
| Param param, |
| @Nullable Object defaultValue) { |
| SkylarkType officialType = null; |
| SkylarkType enforcedType = null; |
| if (param.type() != Object.class) { |
| if (param.generic1() != Object.class) { |
| // Enforce the proper parametric type for Starlark 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.put(param.name(), enforcedType) != null) { |
| throw new IllegalStateException( |
| String.format("duplicate parameter %s on method %s", param.name(), name)); |
| } |
| if (param.doc().isEmpty() && documented) { |
| throw new IllegalStateException( |
| String.format("parameter %s on method %s is undocumented", param.name(), name)); |
| } |
| doc.put(param.name(), param.doc()); |
| if (defaultValue != null && !defaultValue.equals(Starlark.UNBOUND) && 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 officialType; |
| } |
| |
| static Object getDefaultValue(Param param) { |
| return getDefaultValue(param.name(), param.defaultValue()); |
| } |
| |
| static Object getDefaultValue(String paramName, String paramDefaultValue) { |
| if (paramDefaultValue.isEmpty()) { |
| return Starlark.NONE; |
| } else { |
| try { |
| Object defaultValue = defaultValueCache.get(paramDefaultValue); |
| if (defaultValue != null) { |
| return defaultValue; |
| } |
| try (Mutability mutability = Mutability.create("initialization")) { |
| // Note that this Skylark thread ignores command line flags. |
| StarlarkThread thread = |
| StarlarkThread.builder(mutability) |
| .useDefaultSemantics() |
| .setGlobals(Module.createForBuiltins(Starlark.UNIVERSE)) |
| .build() |
| .update("unbound", Starlark.UNBOUND); |
| defaultValue = EvalUtils.eval(ParserInput.fromLines(paramDefaultValue), thread); |
| defaultValueCache.put(paramDefaultValue, defaultValue); |
| return defaultValue; |
| } |
| } catch (Exception e) { |
| throw new RuntimeException( |
| String.format( |
| "Exception while processing @Param %s, default value %s", |
| paramName, paramDefaultValue), |
| e); |
| } |
| } |
| } |
| |
| } |