blob: b5aa6c972944c185877bf6f2af3f24ec9a97f878 [file] [log] [blame]
// 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)));
}
}
}