// 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.Throwables; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.ProfilerTask; import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature; import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType; import com.google.devtools.build.lib.util.Preconditions; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.NoSuchElementException; import javax.annotation.Nullable; /** * A class for Skylark functions provided as builtins by the Skylark implementation */ public class BuiltinFunction extends BaseFunction { /** ExtraArgKind so you can tweek your function's own calling convention */ public static enum ExtraArgKind { LOCATION, SYNTAX_TREE, ENVIRONMENT; } // Predefined system add-ons to function signatures public static final ExtraArgKind[] USE_LOC = new ExtraArgKind[] {ExtraArgKind.LOCATION}; public static final ExtraArgKind[] USE_LOC_ENV = new ExtraArgKind[] {ExtraArgKind.LOCATION, ExtraArgKind.ENVIRONMENT}; public static final ExtraArgKind[] USE_AST = new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE}; public static final ExtraArgKind[] USE_AST_ENV = new ExtraArgKind[] {ExtraArgKind.SYNTAX_TREE, ExtraArgKind.ENVIRONMENT}; // The underlying invoke() method. @Nullable private Method invokeMethod; // extra arguments required beside signature. @Nullable private ExtraArgKind[] extraArgs; // The count of arguments in the inner invoke method, // to be used as size of argument array by the outer call method. private int innerArgumentCount; // The returnType of the method. private Class<?> returnType; /** Create unconfigured function from its name */ public BuiltinFunction(String name) { super(name); } /** Creates an unconfigured BuiltinFunction with the given name and defaultValues */ public BuiltinFunction(String name, Iterable<Object> defaultValues) { super(name, defaultValues); } /** Creates a BuiltinFunction with the given name and signature */ public BuiltinFunction(String name, FunctionSignature signature) { super(name, signature); configure(); } /** Creates a BuiltinFunction with the given name and signature with values */ public BuiltinFunction(String name, FunctionSignature.WithValues<Object, SkylarkType> signature) { super(name, signature); configure(); } /** Creates a BuiltinFunction with the given name and signature and extra arguments */ public BuiltinFunction(String name, FunctionSignature signature, ExtraArgKind[] extraArgs) { super(name, signature); this.extraArgs = extraArgs; configure(); } /** Creates a BuiltinFunction with the given name, signature with values, and extra arguments */ public BuiltinFunction(String name, FunctionSignature.WithValues<Object, SkylarkType> signature, ExtraArgKind[] extraArgs) { super(name, signature); this.extraArgs = extraArgs; configure(); } /** Creates a BuiltinFunction from the given name and a Factory */ public BuiltinFunction(String name, Factory factory) { super(name); configure(factory); } @Override protected int getArgArraySize () { return innerArgumentCount; } protected ExtraArgKind[] getExtraArgs () { return extraArgs; } @Override @Nullable public Object call(Object[] args, FuncallExpression ast, Environment env) throws EvalException, InterruptedException { Preconditions.checkNotNull(env); // ast is null when called from Java (as there's no Skylark call site). Location loc = ast == null ? Location.BUILTIN : ast.getLocation(); // Add extra arguments, if needed if (extraArgs != null) { int i = args.length - extraArgs.length; for (BuiltinFunction.ExtraArgKind extraArg : extraArgs) { switch(extraArg) { case LOCATION: args[i] = loc; break; case SYNTAX_TREE: args[i] = ast; break; case ENVIRONMENT: args[i] = env; break; } i++; } } Profiler.instance().startTask(ProfilerTask.SKYLARK_BUILTIN_FN, getName()); // Last but not least, actually make an inner call to the function with the resolved arguments. try { env.enterScope(this, ast, env.getGlobals()); return invokeMethod.invoke(this, args); } catch (InvocationTargetException x) { Throwable e = x.getCause(); if (e instanceof EvalException) { throw ((EvalException) e).ensureLocation(loc); } else if (e instanceof IllegalArgumentException) { throw new EvalException(loc, "illegal argument in call to " + getName(), e); } // TODO(bazel-team): replace with Throwables.throwIfInstanceOf once Guava 20 is released. Throwables.propagateIfInstanceOf(e, InterruptedException.class); // TODO(bazel-team): replace with Throwables.throwIfUnchecked once Guava 20 is released. Throwables.propagateIfPossible(e); throw badCallException(loc, e, args); } catch (IllegalArgumentException e) { // Either this was thrown by Java itself, or it's a bug // To cover the first case, let's manually check the arguments. final int len = args.length - ((extraArgs == null) ? 0 : extraArgs.length); final Class<?>[] types = invokeMethod.getParameterTypes(); for (int i = 0; i < args.length; i++) { if (args[i] != null && !types[i].isAssignableFrom(args[i].getClass())) { String paramName = i < len ? signature.getSignature().getNames().get(i) : extraArgs[i - len].name(); int extraArgsCount = (extraArgs == null) ? 0 : extraArgs.length; throw new EvalException( loc, String.format( "method %s is not applicable for arguments %s: " + "'%s' is '%s', but should be '%s'", getShortSignature(true), printTypeString(args, args.length - extraArgsCount), paramName, EvalUtils.getDataTypeName(args[i]), EvalUtils.getDataTypeNameFromClass(types[i]))); } } throw badCallException(loc, e, args); } catch (IllegalAccessException e) { throw badCallException(loc, e, args); } finally { Profiler.instance().completeTask(ProfilerTask.SKYLARK_BUILTIN_FN); env.exitScope(); } } private static String stacktraceToString(StackTraceElement[] elts) { StringBuilder b = new StringBuilder(); for (StackTraceElement e : elts) { b.append(e); b.append("\n"); } return b.toString(); } private IllegalStateException badCallException(Location loc, Throwable e, Object... args) { // If this happens, it's a bug in our code. return new IllegalStateException( String.format( "%s%s (%s)\n" + "while calling %s with args %s\n" + "Java parameter types: %s\nSkylark type checks: %s", (loc == null) ? "" : loc + ": ", Arrays.asList(args), e.getClass().getName(), stacktraceToString(e.getStackTrace()), this, Arrays.asList(invokeMethod.getParameterTypes()), signature.getTypes()), e); } /** Configure the reflection mechanism */ @Override public void configure(SkylarkSignature annotation) { Preconditions.checkState(!isConfigured()); // must not be configured yet enforcedArgumentTypes = new ArrayList<>(); this.extraArgs = SkylarkSignatureProcessor.getExtraArgs(annotation); this.returnType = annotation.returnType(); super.configure(annotation); } // finds the method and makes it accessible (which is needed to find it, and later to use it) protected Method findMethod(final String name) { Method found = null; for (Method method : this.getClass().getDeclaredMethods()) { method.setAccessible(true); if (name.equals(method.getName())) { if (found != null) { throw new IllegalArgumentException(String.format( "function %s has more than one method named %s", getName(), name)); } found = method; } } if (found == null) { throw new NoSuchElementException(String.format( "function %s doesn't have a method named %s", getName(), name)); } return found; } /** Configure the reflection mechanism */ @Override protected void configure() { invokeMethod = findMethod("invoke"); int arguments = signature.getSignature().getShape().getArguments(); innerArgumentCount = arguments + (extraArgs == null ? 0 : extraArgs.length); Class<?>[] parameterTypes = invokeMethod.getParameterTypes(); if (innerArgumentCount != parameterTypes.length) { // Guard message construction by check to avoid autoboxing two integers. throw new IllegalStateException( String.format( "bad argument count for %s: method has %s arguments, type list has %s", getName(), innerArgumentCount, parameterTypes.length)); } if (enforcedArgumentTypes != null) { for (int i = 0; i < arguments; i++) { SkylarkType enforcedType = enforcedArgumentTypes.get(i); if (enforcedType != null) { Class<?> parameterType = parameterTypes[i]; String msg = String.format( "fun %s(%s), param %s, enforcedType: %s (%s); parameterType: %s", getName(), signature, signature.getSignature().getNames().get(i), enforcedType, enforcedType.getType(), parameterType); if (enforcedType instanceof SkylarkType.Simple || enforcedType instanceof SkylarkFunctionType) { Preconditions.checkArgument( enforcedType.getType() == parameterType, msg); // No need to enforce Simple types on the Skylark side, the JVM will do it for us. enforcedArgumentTypes.set(i, null); } else if (enforcedType instanceof SkylarkType.Combination) { Preconditions.checkArgument(enforcedType.getType() == parameterType, msg); } else { Preconditions.checkArgument( parameterType == Object.class || parameterType == null, msg); } } } } // No need for the enforcedArgumentTypes List if all the types were Simple enforcedArgumentTypes = FunctionSignature.<SkylarkType>valueListOrNull(enforcedArgumentTypes); if (returnType != null) { Class<?> type = returnType; Class<?> methodReturnType = invokeMethod.getReturnType(); Preconditions.checkArgument(type == methodReturnType, "signature for function %s says it returns %s but its invoke method returns %s", getName(), returnType, methodReturnType); } } /** Configure by copying another function's configuration */ // Alternatively, we could have an extension BuiltinFunctionSignature of FunctionSignature, // and use *that* instead of a Factory. public void configure(BuiltinFunction.Factory factory) { // this function must not be configured yet, but the factory must be Preconditions.checkState(!isConfigured()); Preconditions.checkState(factory.isConfigured(), "function factory is not configured for %s", getName()); this.paramDoc = factory.getParamDoc(); this.signature = factory.getSignature(); this.extraArgs = factory.getExtraArgs(); this.objectType = factory.getObjectType(); configure(); } /** * A Factory allows for a @SkylarkSignature annotation to be provided and processed in advance * for a function that will be defined later as a closure (see e.g. in PackageFactory). * * <p>Each instance of this class must define a method create that closes over some (final) * variables and returns a BuiltinFunction. */ public abstract static class Factory extends BuiltinFunction { @Nullable private Method createMethod; /** Create unconfigured function Factory from its name */ public Factory(String name) { super(name); } /** Creates an unconfigured function Factory with the given name and defaultValues */ public Factory(String name, Iterable<Object> defaultValues) { super(name, defaultValues); } @Override public void configure() { if (createMethod != null) { return; } createMethod = findMethod("create"); } @Override public Object call(Object[] args, @Nullable FuncallExpression ast, @Nullable Environment env) throws EvalException { throw new EvalException(null, "tried to invoke a Factory for function " + this); } /** Instantiate the Factory * @param args arguments to pass to the create method * @return a new BuiltinFunction that closes over the arguments */ public BuiltinFunction apply(Object... args) { try { return (BuiltinFunction) createMethod.invoke(this, args); } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(String.format( "Exception while applying BuiltinFunction.Factory %s: %s", this, e.getMessage()), e); } } } }