diff options
Diffstat (limited to 'src/main/java')
3 files changed, 117 insertions, 8 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkinterface/SkylarkCallable.java b/src/main/java/com/google/devtools/build/lib/skylarkinterface/SkylarkCallable.java index 92ef0a3a87..df3387a26a 100644 --- a/src/main/java/com/google/devtools/build/lib/skylarkinterface/SkylarkCallable.java +++ b/src/main/java/com/google/devtools/build/lib/skylarkinterface/SkylarkCallable.java @@ -111,6 +111,18 @@ public @interface SkylarkCallable { Param extraKeywords() default @Param(name = ""); /** + * If true, indicates that the class containing the annotated method has the ability to be called + * from skylark (as if it were a function) and that the annotated method should be invoked when + * this occurs. + * + * <p>A class may only have one method with selfCall set to true.</p> + * + * <p>A method with selfCall=true must not be a structField, and must have name specified + * (used for descriptive errors if, for example, there are missing arguments).</p> + */ + boolean selfCall() default false; + + /** * Set it to true if the Java method may return <code>null</code> (which will then be converted to * <code>None</code>). If not set and the Java method returns null, an error will be raised. */ diff --git a/src/main/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessor.java b/src/main/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessor.java index cfc8958762..71df625cba 100644 --- a/src/main/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessor.java +++ b/src/main/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessor.java @@ -16,6 +16,7 @@ package com.google.devtools.build.lib.skylarkinterface.processor; import com.google.devtools.build.lib.skylarkinterface.Param; import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; +import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.AbstractProcessor; @@ -60,6 +61,9 @@ import javax.tools.Diagnostic; * positional parameters with default values. * </li> * <li>Either the doc string is non-empty, or documented is false.</li> + * <li>Each class may only have one annotated method with selfCall=true.</li> + * <li>A method annotated with selfCall=true must have a non-empty name.</li> + * <li>A method annotated with selfCall=true must have structField=false.</li> * </ul> * * <p>These properties can be relied upon at runtime without additional checks. @@ -68,6 +72,8 @@ import javax.tools.Diagnostic; public final class SkylarkCallableProcessor extends AbstractProcessor { private Messager messager; + private Set<String> classesWithSelfcall; + private static final String SKYLARK_LIST = "com.google.devtools.build.lib.syntax.SkylarkList<?>"; private static final String SKYLARK_DICT = "com.google.devtools.build.lib.syntax.SkylarkDict<?,?>"; @@ -86,6 +92,7 @@ public final class SkylarkCallableProcessor extends AbstractProcessor { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); messager = processingEnv.getMessager(); + classesWithSelfcall = new HashSet<>(); } @Override @@ -107,6 +114,7 @@ public final class SkylarkCallableProcessor extends AbstractProcessor { verifyParamSemantics(methodElement, annotation); verifyNumberOfParameters(methodElement, annotation); verifyExtraInterpreterParams(methodElement, annotation); + verifyIfSelfCall(methodElement, annotation); } catch (SkylarkCallableProcessorException exception) { error(exception.methodElement, exception.errorMessage); } @@ -115,12 +123,33 @@ public final class SkylarkCallableProcessor extends AbstractProcessor { return true; } + private void verifyIfSelfCall(ExecutableElement methodElement, SkylarkCallable annotation) + throws SkylarkCallableProcessorException { + if (annotation.selfCall()) { + if (annotation.structField()) { + throw new SkylarkCallableProcessorException( + methodElement, + "@SkylarkCallable-annotated methods with selfCall=true must have structField=false"); + } + if (annotation.name().isEmpty()) { + throw new SkylarkCallableProcessorException( + methodElement, + "@SkylarkCallable-annotated methods with selfCall=true must have a name"); + } + if (!classesWithSelfcall.add(methodElement.getEnclosingElement().asType().toString())) { + throw new SkylarkCallableProcessorException( + methodElement, + "Containing class has more than one selfCall method defined."); + } + } + } + private void verifyDocumented(ExecutableElement methodElement, SkylarkCallable annotation) throws SkylarkCallableProcessorException { if (annotation.documented() && annotation.doc().isEmpty()) { throw new SkylarkCallableProcessorException( - methodElement, - "The 'doc' string must be non-empty if 'documented' is true."); + methodElement, + "The 'doc' string must be non-empty if 'documented' is true."); } } @@ -162,8 +191,8 @@ public final class SkylarkCallableProcessor extends AbstractProcessor { throw new SkylarkCallableProcessorException( methodElement, String.format("Parameter '%s' has 'None' default value but is not noneable. " - + "(If this is intended as a mandatory parameter, leave the defaultValue field " - + "empty)", + + "(If this is intended as a mandatory parameter, leave the defaultValue field " + + "empty)", parameter.name())); } if ((parameter.allowedTypes().length > 0) @@ -171,8 +200,8 @@ public final class SkylarkCallableProcessor extends AbstractProcessor { throw new SkylarkCallableProcessorException( methodElement, String.format("Parameter '%s' has both 'type' and 'allowedTypes' specified. Only" - + " one may be specified.", - parameter.name())); + + " one may be specified.", + parameter.name())); } if (parameter.positional()) { diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java index dba8ffc92c..236bcc2576 100644 --- a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java +++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java @@ -49,6 +49,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -82,6 +83,33 @@ public final class FuncallExpression extends Expression { } } + private static final LoadingCache<Class<?>, Optional<MethodDescriptor>> selfCallCache = + CacheBuilder.newBuilder() + .build( + new CacheLoader<Class<?>, Optional<MethodDescriptor>>() { + @Override + public Optional<MethodDescriptor> load(Class<?> key) throws Exception { + MethodDescriptor returnValue = null; + for (Method method : key.getMethods()) { + // Synthetic methods lead to false multiple matches + if (method.isSynthetic()) { + continue; + } + SkylarkCallable callable = SkylarkInterfaceUtils.getSkylarkCallable(method); + if (callable != null && callable.selfCall()) { + if (returnValue != null) { + throw new IllegalArgumentException( + String.format( + "Class %s has two selfCall methods defined", + key.getName())); + } + returnValue = new MethodDescriptor(method, callable); + } + } + return Optional.ofNullable(returnValue); + } + }); + private static final LoadingCache<Class<?>, Map<String, List<MethodDescriptor>>> methodCache = CacheBuilder.newBuilder() .build( @@ -99,6 +127,10 @@ public final class FuncallExpression extends Expression { if (callable == null) { continue; } + if (callable.selfCall()) { + // Self-call java methods are not treated as methods of the skylark value. + continue; + } String name = callable.name(); if (name.isEmpty()) { name = StringUtilities.toPythonStyleFunctionName(method.getName()); @@ -330,6 +362,38 @@ public final class FuncallExpression extends Expression { } /** + * Returns true if the given class has a method annotated with {@link SkylarkCallable} + * with {@link SkylarkCallable#selfCall()} set to true. + */ + public static boolean hasSelfCallMethod(Class<?> objClass) { + try { + return selfCallCache.get(objClass).isPresent(); + } catch (ExecutionException e) { + throw new IllegalStateException("Method loading failed: " + e); + } + } + + /** + * Returns a {@link BuiltinCallable} object representing a function which calls the selfCall + * java method of the given object (the {@link SkylarkCallable} method with + * {@link SkylarkCallable#selfCall()} set to true). + * + * @throws IllegalStateException if no such method exists for the object + */ + public static BuiltinCallable getSelfCallMethod(Object obj) { + try { + Optional<MethodDescriptor> selfCallDescriptor = selfCallCache.get(obj.getClass()); + if (!selfCallDescriptor.isPresent()) { + throw new IllegalStateException("Class " + obj.getClass() + " has no selfCall method"); + } + MethodDescriptor descriptor = selfCallDescriptor.get(); + return new BuiltinCallable(descriptor.getAnnotation().name(), obj, descriptor); + } catch (ExecutionException e) { + throw new IllegalStateException("Method loading failed: " + e); + } + } + + /** * Returns a {@link BuiltinCallable} representing a {@link SkylarkCallable}-annotated instance * method of a given object with the given method name. */ @@ -728,14 +792,18 @@ public final class FuncallExpression extends Expression { } /** - * Checks whether the given object is a {@link BaseFunction}. + * Checks whether the given object is callable, either by being a {@link BaseFunction} or having + * a {@link SkylarkCallable}-annotated method with selfCall = true. * - * @throws EvalException If not a BaseFunction. + * @return a BaseFunction object representing the callable function this object represents + * @throws EvalException if the object is not callable. */ private static BaseFunction checkCallable(Object functionValue, Location location) throws EvalException { if (functionValue instanceof BaseFunction) { return (BaseFunction) functionValue; + } else if (hasSelfCallMethod(functionValue.getClass())) { + return getSelfCallMethod(functionValue); } else { throw new EvalException( location, "'" + EvalUtils.getDataTypeName(functionValue) + "' object is not callable"); |