| // Copyright 2014 Google Inc. 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.common.collect.ImmutableMap; |
| import com.google.devtools.build.lib.events.Location; |
| import com.google.devtools.build.lib.packages.Type.ConversionException; |
| import com.google.devtools.build.lib.syntax.EvalException.EvalExceptionWithJavaCause; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.concurrent.ExecutionException; |
| |
| /** |
| * A function class for Skylark built in functions. Supports mandatory and optional arguments. |
| * All usable arguments have to be specified. In case of ambiguous arguments (a parameter is |
| * specified as positional and keyword arguments in the function call) an exception is thrown. |
| */ |
| public abstract class SkylarkFunction extends AbstractFunction { |
| |
| private ImmutableList<String> parameters; |
| private ImmutableMap<String, SkylarkBuiltin.Param> parameterTypes; |
| private int mandatoryParamNum; |
| private boolean configured = false; |
| private Class<?> objectType; |
| private boolean onlyLoadingPhase; |
| |
| /** |
| * Creates a SkylarkFunction with the given name. |
| */ |
| public SkylarkFunction(String name) { |
| super(name); |
| } |
| |
| /** |
| * Configures the parameter of this Skylark function using the annotation. |
| */ |
| public void configure(SkylarkBuiltin annotation) { |
| Preconditions.checkState(!configured); |
| Preconditions.checkArgument( |
| getName().equals(annotation.name()), "%s != %s", getName(), annotation.name()); |
| mandatoryParamNum = 0; |
| ImmutableList.Builder<String> paramListBuilder = ImmutableList.builder(); |
| ImmutableMap.Builder<String, SkylarkBuiltin.Param> paramTypeBuilder = ImmutableMap.builder(); |
| for (SkylarkBuiltin.Param param : annotation.mandatoryParams()) { |
| paramListBuilder.add(param.name()); |
| paramTypeBuilder.put(param.name(), param); |
| mandatoryParamNum++; |
| } |
| for (SkylarkBuiltin.Param param : annotation.optionalParams()) { |
| paramListBuilder.add(param.name()); |
| paramTypeBuilder.put(param.name(), param); |
| } |
| parameters = paramListBuilder.build(); |
| parameterTypes = paramTypeBuilder.build(); |
| this.objectType = annotation.objectType().equals(Object.class) ? null : annotation.objectType(); |
| this.onlyLoadingPhase = annotation.onlyLoadingPhase(); |
| configured = true; |
| } |
| |
| /** |
| * Returns true if the SkylarkFunction is configured. |
| */ |
| public boolean isConfigured() { |
| return configured; |
| } |
| |
| @Override |
| public Class<?> getObjectType() { |
| return objectType; |
| } |
| |
| public boolean isOnlyLoadingPhase() { |
| return onlyLoadingPhase; |
| } |
| |
| @Override |
| public Object call(List<Object> args, |
| Map<String, Object> kwargs, |
| FuncallExpression ast, |
| Environment env) |
| throws EvalException, InterruptedException { |
| Preconditions.checkState(configured, "Function %s was not configured", getName()); |
| try { |
| ImmutableMap.Builder<String, Object> arguments = new ImmutableMap.Builder<>(); |
| if (objectType != null && !FuncallExpression.isNamespace(objectType)) { |
| args = new ArrayList<>(args); // args immutable, get a mutable copy. |
| arguments.put("self", args.remove(0)); |
| } |
| |
| int maxParamNum = parameters.size(); |
| int paramNum = args.size() + kwargs.size(); |
| |
| if (paramNum < mandatoryParamNum) { |
| throw new EvalException(ast.getLocation(), |
| String.format("incorrect number of arguments (got %s, expected at least %s)", |
| paramNum, mandatoryParamNum)); |
| } else if (paramNum > maxParamNum) { |
| throw new EvalException(ast.getLocation(), |
| String.format("incorrect number of arguments (got %s, expected at most %s)", |
| paramNum, maxParamNum)); |
| } |
| |
| for (int i = 0; i < mandatoryParamNum; i++) { |
| Preconditions.checkState(i < args.size() || kwargs.containsKey(parameters.get(i)), |
| String.format("missing mandatory parameter: %s", parameters.get(i))); |
| } |
| |
| for (int i = 0; i < args.size(); i++) { |
| checkTypeAndAddArg(parameters.get(i), args.get(i), arguments, ast.getLocation()); |
| } |
| |
| for (Entry<String, Object> kwarg : kwargs.entrySet()) { |
| int idx = parameters.indexOf(kwarg.getKey()); |
| if (idx < 0) { |
| throw new EvalException(ast.getLocation(), |
| String.format("unknown keyword argument: %s", kwarg.getKey())); |
| } |
| if (idx < args.size()) { |
| throw new EvalException(ast.getLocation(), |
| String.format("ambiguous argument: %s", kwarg.getKey())); |
| } |
| checkTypeAndAddArg(kwarg.getKey(), kwarg.getValue(), arguments, ast.getLocation()); |
| } |
| |
| return call(arguments.build(), ast, env); |
| } catch (ConversionException | IllegalArgumentException | IllegalStateException |
| | ClassCastException | ClassNotFoundException | ExecutionException e) { |
| if (e.getMessage() != null) { |
| throw new EvalException(ast.getLocation(), e.getMessage()); |
| } else { |
| // TODO(bazel-team): ideally this shouldn't happen, however we need this for debugging |
| throw new EvalExceptionWithJavaCause(ast.getLocation(), e); |
| } |
| } |
| } |
| |
| private void checkTypeAndAddArg(String paramName, Object value, |
| ImmutableMap.Builder<String, Object> arguments, Location loc) throws EvalException { |
| SkylarkBuiltin.Param param = parameterTypes.get(paramName); |
| if (param.callbackEnabled() && value instanceof Function) { |
| // If we pass a function as an argument we trust the Function implementation with the type |
| // check. It's OK since the function needs to be called manually anyway. |
| arguments.put(paramName, value); |
| return; |
| } |
| checkType(getName(), paramName, SkylarkType.of(param.type(), param.generic1()), |
| value, loc, param.doc()); |
| arguments.put(paramName, value); |
| } |
| |
| public static void checkType(String functionName, String paramName, |
| SkylarkType type, Object value, Location loc, String paramDoc) throws EvalException { |
| if (type != null && value != null) { // TODO(bazel-team): should we give a pass to NONE here? |
| if (!type.contains(value)) { |
| throw new EvalException(loc, String.format( |
| "expected %s for '%s' while calling %s but got %s instead: %s", |
| type, paramName, functionName, EvalUtils.getDataTypeName(value, true), value)); |
| } |
| } |
| } |
| |
| /** |
| * The actual function call. All positional and keyword arguments are put in the |
| * arguments map. |
| */ |
| protected abstract Object call( |
| Map<String, Object> arguments, FuncallExpression ast, Environment env) throws EvalException, |
| ConversionException, |
| IllegalArgumentException, |
| IllegalStateException, |
| InterruptedException, |
| ClassCastException, |
| ClassNotFoundException, |
| ExecutionException; |
| |
| /** |
| * An intermediate class to provide a simpler interface for Skylark functions. |
| */ |
| public abstract static class SimpleSkylarkFunction extends SkylarkFunction { |
| |
| public SimpleSkylarkFunction(String name) { |
| super(name); |
| } |
| |
| @Override |
| protected final Object call( |
| Map<String, Object> arguments, FuncallExpression ast, Environment env) throws EvalException, |
| ConversionException, |
| IllegalArgumentException, |
| IllegalStateException, |
| ClassCastException, |
| ExecutionException { |
| return call(arguments, ast.getLocation()); |
| } |
| |
| /** |
| * The actual function call. All positional and keyword arguments are put in the |
| * arguments map. |
| */ |
| protected abstract Object call(Map<String, Object> arguments, Location loc) |
| throws EvalException, |
| ConversionException, |
| IllegalArgumentException, |
| IllegalStateException, |
| ClassCastException, |
| ExecutionException; |
| } |
| |
| // TODO(bazel-team): this is only used in MixedModeFunctions in MethodLibrary, migrate those |
| // to SkylarkFunction then remove this. |
| public static <TYPE> TYPE cast(Object elem, Class<TYPE> type, String what, Location loc) |
| throws EvalException { |
| try { |
| return type.cast(elem); |
| } catch (ClassCastException e) { |
| throw new EvalException(loc, String.format("expected %s for '%s' but got %s instead", |
| EvalUtils.getDataTypeNameFromClass(type), what, EvalUtils.getDataTypeName(elem))); |
| } |
| } |
| } |