blob: e564043f6be59373dacef79a64276a54ecbf8410 [file] [log] [blame]
// Copyright 2018 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.Preconditions;
import com.google.common.base.Throwables;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.annotation.Nullable;
import net.starlark.java.annot.StarlarkMethod;
/**
* A value class to store Methods with their corresponding {@link StarlarkMethod} annotation
* metadata. This is needed because the annotation is sometimes in a superclass.
*
* <p>The annotation metadata is duplicated in this class to avoid usage of Java dynamic proxies
* which are ~7× slower.
*/
final class MethodDescriptor {
private final Method method;
private final StarlarkMethod annotation;
private final String name;
private final String doc;
private final boolean documented;
private final boolean structField;
private final ParamDescriptor[] parameters;
private final boolean extraPositionals;
private final boolean extraKeywords;
private final boolean selfCall;
private final boolean allowReturnNones;
private final boolean useStarlarkThread;
private final boolean useStarlarkSemantics;
private MethodDescriptor(
Method method,
StarlarkMethod annotation,
String name,
String doc,
boolean documented,
boolean structField,
ParamDescriptor[] parameters,
boolean extraPositionals,
boolean extraKeywords,
boolean selfCall,
boolean allowReturnNones,
boolean useStarlarkThread,
boolean useStarlarkSemantics) {
this.method = method;
this.annotation = annotation;
this.name = name;
this.doc = doc;
this.documented = documented;
this.structField = structField;
this.parameters = parameters;
this.extraPositionals = extraPositionals;
this.extraKeywords = extraKeywords;
this.selfCall = selfCall;
this.allowReturnNones = allowReturnNones;
this.useStarlarkThread = useStarlarkThread;
this.useStarlarkSemantics = useStarlarkSemantics;
}
/** Returns the StarlarkMethod annotation corresponding to this method. */
StarlarkMethod getAnnotation() {
return annotation;
}
/** @return Starlark method descriptor for provided Java method and signature annotation. */
static MethodDescriptor of(
Method method, StarlarkMethod annotation, StarlarkSemantics semantics) {
// This happens when the interface is public but the implementation classes
// have reduced visibility.
method.setAccessible(true);
return new MethodDescriptor(
method,
annotation,
annotation.name(),
annotation.doc(),
annotation.documented(),
annotation.structField(),
Arrays.stream(annotation.parameters())
.map(param -> ParamDescriptor.of(param, semantics))
.toArray(ParamDescriptor[]::new),
!annotation.extraPositionals().name().isEmpty(),
!annotation.extraKeywords().name().isEmpty(),
annotation.selfCall(),
annotation.allowReturnNones(),
annotation.useStarlarkThread(),
annotation.useStarlarkSemantics());
}
private static final Object[] EMPTY = {};
/** Calls this method, which must have {@code structField=true}. */
Object callField(Object obj, StarlarkSemantics semantics, @Nullable Mutability mu)
throws EvalException, InterruptedException {
if (!structField) {
throw new IllegalStateException("not a struct field: " + name);
}
Object[] args = useStarlarkSemantics ? new Object[] {semantics} : EMPTY;
return call(obj, args, mu);
}
/**
* Invokes this method using {@code obj} as a target and {@code args} as Java arguments.
*
* <p>Methods with {@code void} return type return {@code None} following Python convention.
*
* <p>The Mutability is used if it is necessary to allocate a Starlark copy of a Java result.
*/
Object call(Object obj, Object[] args, @Nullable Mutability mu)
throws EvalException, InterruptedException {
Preconditions.checkNotNull(obj);
Object result;
try {
result = method.invoke(obj, args);
} catch (IllegalAccessException ex) {
// "Can't happen": the annotated processor ensures that annotated methods are accessible.
throw new IllegalStateException(ex);
} catch (IllegalArgumentException ex) {
// "Can't happen": unexpected type mismatch.
// Show details to aid debugging (see e.g. b/162444744).
StringBuilder buf = new StringBuilder();
buf.append(
String.format(
"IllegalArgumentException (%s) in Starlark call of %s, obj=%s (%s), args=[",
ex.getMessage(), method, Starlark.repr(obj), Starlark.type(obj)));
String sep = "";
for (Object arg : args) {
buf.append(String.format("%s%s (%s)", sep, Starlark.repr(arg), Starlark.type(arg)));
sep = ", ";
}
buf.append(']');
throw new IllegalArgumentException(buf.toString());
} catch (InvocationTargetException ex) {
Throwable e = ex.getCause();
if (e == null) {
throw new IllegalStateException(e);
}
// Don't intercept unchecked exceptions.
Throwables.throwIfUnchecked(e);
if (e instanceof EvalException) {
throw (EvalException) e;
} else if (e instanceof InterruptedException) {
throw (InterruptedException) e;
} else {
// All other checked exceptions (e.g. LabelSyntaxException) are reported to Starlark.
throw new EvalException(e);
}
}
if (method.getReturnType().equals(Void.TYPE)) {
return Starlark.NONE;
}
if (result == null) {
// TODO(adonovan): eliminate allowReturnNones. Given that we convert
// String/Integer/Boolean/List/Map, it seems obtuse to crash instead
// of converting null too.
if (isAllowReturnNones()) {
return Starlark.NONE;
} else {
throw new IllegalStateException(
"method invocation returned null: " + getName() + Tuple.copyOf(Arrays.asList(args)));
}
}
return Starlark.fromJava(result, mu);
}
/** @see StarlarkMethod#name() */
String getName() {
return name;
}
Method getMethod() {
return method;
}
/** @see StarlarkMethod#structField() */
boolean isStructField() {
return structField;
}
/** @see StarlarkMethod#useStarlarkThread() */
boolean isUseStarlarkThread() {
return useStarlarkThread;
}
/** @see StarlarkMethod#useStarlarkSemantics() */
boolean isUseStarlarkSemantics() {
return useStarlarkSemantics;
}
/** @see StarlarkMethod#allowReturnNones() */
boolean isAllowReturnNones() {
return allowReturnNones;
}
/** @return {@code true} if this method accepts extra arguments ({@code *args}) */
boolean acceptsExtraArgs() {
return extraPositionals;
}
/** @see StarlarkMethod#extraKeywords() */
boolean acceptsExtraKwargs() {
return extraKeywords;
}
/** @see StarlarkMethod#parameters() */
ParamDescriptor[] getParameters() {
return parameters;
}
/** Returns the index of the named parameter or -1 if not found. */
int getParameterIndex(String name) {
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].getName().equals(name)) {
return i;
}
}
return -1;
}
/** @see StarlarkMethod#documented() */
boolean isDocumented() {
return documented;
}
/** @see StarlarkMethod#doc() */
String getDoc() {
return doc;
}
/** @see StarlarkMethod#selfCall() */
boolean isSelfCall() {
return selfCall;
}
}