aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkinterface/SkylarkCallable.java12
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessor.java41
-rw-r--r--src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java72
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");