Enforce via annotation processor that a class may not have two @SkylarkCallable methods with the same name.
This greatly simplifies the code around method retrieval.
Fixes #6490.
RELNOTES: None.
PiperOrigin-RevId: 218568560
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 aaade02..0ef2b61 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
@@ -14,6 +14,8 @@
package com.google.devtools.build.lib.skylarkinterface.processor;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.SetMultimap;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.syntax.SkylarkSemantics.FlagIdentifier;
@@ -68,7 +70,11 @@
public final class SkylarkCallableProcessor extends AbstractProcessor {
private Messager messager;
+ // A set containing the names of all classes which have a method with @SkylarkCallable.selfCall.
private Set<String> classesWithSelfcall;
+ // A multimap where keys are class names, and values are the callable method names identified in
+ // that class (where "method name" is @SkylarkCallable.name").
+ private SetMultimap<String, String> processedClassMethods;
private static final String SKYLARK_LIST = "com.google.devtools.build.lib.syntax.SkylarkList<?>";
private static final String SKYLARK_DICT =
@@ -89,6 +95,7 @@
super.init(processingEnv);
messager = processingEnv.getMessager();
classesWithSelfcall = new HashSet<>();
+ processedClassMethods = LinkedHashMultimap.create();
}
@Override
@@ -113,6 +120,7 @@
verifyExtraInterpreterParams(methodElement, annotation);
verifyIfSelfCall(methodElement, annotation);
verifyFlagToggles(methodElement, annotation);
+ verifyNoNameConflict(methodElement, annotation);
} catch (SkylarkCallableProcessorException exception) {
error(exception.methodElement, exception.errorMessage);
}
@@ -121,6 +129,20 @@
return true;
}
+ private void verifyNoNameConflict(ExecutableElement methodElement, SkylarkCallable annotation)
+ throws SkylarkCallableProcessorException {
+ boolean methodNameIsUniqueForClass =
+ processedClassMethods.put(
+ methodElement.getEnclosingElement().asType().toString(),
+ annotation.name());
+ if (!methodNameIsUniqueForClass) {
+ throw new SkylarkCallableProcessorException(
+ methodElement,
+ String.format("Containing class has more than one method with name '%s' defined.",
+ annotation.name()));
+ }
+ }
+
private void verifyFlagToggles(ExecutableElement methodElement, SkylarkCallable annotation)
throws SkylarkCallableProcessorException {
if (annotation.enableOnlyWithFlag() != FlagIdentifier.NONE
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
index cee5ab2..0a31aaa 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
@@ -13,11 +13,9 @@
// limitations under the License.
package com.google.devtools.build.lib.syntax;
-import com.google.common.collect.Streams;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.util.SpellChecker;
import java.io.IOException;
-import java.util.Optional;
/** Syntax node for a dot expression. e.g. obj.field, but not obj.method() */
public final class DotExpression extends Expression {
@@ -118,24 +116,19 @@
public static Object eval(Object objValue, String name,
Location loc, Environment env) throws EvalException, InterruptedException {
- Iterable<MethodDescriptor> methods =
+ MethodDescriptor method =
objValue instanceof Class<?>
- ? FuncallExpression.getMethods(env.getSemantics(), (Class<?>) objValue, name)
- : FuncallExpression.getMethods(env.getSemantics(), objValue.getClass(), name);
+ ? FuncallExpression.getMethod(env.getSemantics(), (Class<?>) objValue, name)
+ : FuncallExpression.getMethod(env.getSemantics(), objValue.getClass(), name);
- if (methods != null) {
- Optional<MethodDescriptor> method =
- Streams.stream(methods).filter(MethodDescriptor::isStructField).findFirst();
- if (method.isPresent() && method.get().isStructField()) {
- return method
- .get()
- .call(
- objValue,
- FuncallExpression.extraInterpreterArgs(method.get(), /* ast = */ null, loc, env)
- .toArray(),
- loc,
- env);
- }
+ if (method != null && method.isStructField()) {
+ return method
+ .call(
+ objValue,
+ FuncallExpression.extraInterpreterArgs(method, /* ast = */ null, loc, env)
+ .toArray(),
+ loc,
+ env);
}
if (objValue instanceof SkylarkClassObject) {
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 d906f8b..dcf038f 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
@@ -25,7 +25,6 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
@@ -39,7 +38,6 @@
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -126,18 +124,19 @@
}
});
- private static final LoadingCache<MethodDescriptorKey, Map<String, List<MethodDescriptor>>>
+ private static final LoadingCache<MethodDescriptorKey, Map<String, MethodDescriptor>>
methodCache =
CacheBuilder.newBuilder()
.build(
- new CacheLoader<MethodDescriptorKey, Map<String, List<MethodDescriptor>>>() {
+ new CacheLoader<MethodDescriptorKey, Map<String, MethodDescriptor>>() {
@Override
- public Map<String, List<MethodDescriptor>> load(MethodDescriptorKey key)
+ public Map<String, MethodDescriptor> load(MethodDescriptorKey key)
throws Exception {
Class<?> keyClass = key.getClazz();
SkylarkSemantics semantics = key.getSemantics();
- Map<String, List<MethodDescriptor>> methodMap = new HashMap<>();
+ ImmutableMap.Builder<String, MethodDescriptor> methodMap =
+ ImmutableMap.builder();
for (Method method : keyClass.getMethods()) {
// Synthetic methods lead to false multiple matches
if (method.isSynthetic()) {
@@ -153,16 +152,10 @@
}
if (semantics.isFeatureEnabledBasedOnTogglingFlags(
callable.enableOnlyWithFlag(), callable.disableWithFlag())) {
- String name = callable.name();
- if (methodMap.containsKey(name)) {
- methodMap.get(name).add(MethodDescriptor.of(method, callable));
- } else {
- methodMap.put(
- name, Lists.newArrayList(MethodDescriptor.of(method, callable)));
- }
+ methodMap.put(callable.name(), MethodDescriptor.of(method, callable));
}
}
- return ImmutableMap.copyOf(methodMap);
+ return methodMap.build();
}
});
@@ -178,7 +171,6 @@
HashSet<String> fieldNamesForCollisions = new HashSet<>();
List<MethodDescriptor> fieldMethods =
methodCache.get(key).values().stream()
- .flatMap(List::stream)
.filter(MethodDescriptor::isStructField)
.collect(Collectors.toList());
@@ -385,12 +377,12 @@
* @deprecated use {@link #getMethods(SkylarkSemantics, Class, String)} instead
*/
@Deprecated
- public static List<MethodDescriptor> getMethods(Class<?> objClass, String methodName) {
- return getMethods(SkylarkSemantics.DEFAULT_SEMANTICS, objClass, methodName);
+ public static MethodDescriptor getMethod(Class<?> objClass, String methodName) {
+ return getMethod(SkylarkSemantics.DEFAULT_SEMANTICS, objClass, methodName);
}
/** Returns the list of Skylark callable Methods of objClass with the given name. */
- public static List<MethodDescriptor> getMethods(
+ public static MethodDescriptor getMethod(
SkylarkSemantics semantics, Class<?> objClass, String methodName) {
try {
return methodCache
@@ -465,13 +457,13 @@
*/
public static BuiltinCallable getBuiltinCallable(Object obj, String methodName) {
Class<?> objClass = obj.getClass();
- List<MethodDescriptor> methodDescriptors = getMethods(objClass, methodName);
- if (methodDescriptors.size() != 1) {
+ MethodDescriptor methodDescriptor = getMethod(objClass, methodName);
+ if (methodDescriptor == null) {
throw new IllegalStateException(String.format(
- "Expected exactly 1 method named '%s' in %s, but found %s",
- methodName, objClass, methodDescriptors.size()));
+ "Expected a method named '%s' in %s, but found none",
+ methodName, objClass));
}
- return new BuiltinCallable(methodName, obj, methodDescriptors.get(0));
+ return new BuiltinCallable(methodName, obj, methodDescriptor);
}
/**
@@ -499,10 +491,7 @@
return methodDescriptor.call(obj, new Object[0], Location.BUILTIN, null);
}
- // TODO(bazel-team): If there's exactly one usable method, this works. If there are multiple
- // matching methods, it still can be a problem. Figure out how the Java compiler does it
- // exactly and copy that behaviour.
- // Throws an EvalException when it cannot find a matching function.
+ // Throws an EvalException when there is no function matching the given name and arguments.
private Pair<MethodDescriptor, List<Object>> findJavaMethod(
Class<?> objClass,
String methodName,
@@ -510,57 +499,36 @@
Map<String, Object> kwargs,
Environment environment)
throws EvalException {
- Pair<MethodDescriptor, List<Object>> matchingMethod = null;
- List<MethodDescriptor> methods = getMethods(environment.getSemantics(), objClass, methodName);
+ MethodDescriptor method = getMethod(environment.getSemantics(), objClass, methodName);
ArgumentListConversionResult argumentListConversionResult = null;
- if (methods != null) {
- for (MethodDescriptor method : methods) {
- if (method.isStructField()) {
- // This indicates a built-in structField which returns a function which may have
- // one or more arguments itself. For example, foo.bar('baz'), where foo.bar is a
- // structField returning a function. Calling the "bar" callable of foo should
- // not have 'baz' propagated, though extra interpreter arguments should be supplied.
- return new Pair<>(method, extraInterpreterArgs(method, null, getLocation(), environment));
+ if (method != null) {
+ if (method.isStructField()) {
+ // This indicates a built-in structField which returns a function which may have
+ // one or more arguments itself. For example, foo.bar('baz'), where foo.bar is a
+ // structField returning a function. Calling the "bar" callable of foo should
+ // not have 'baz' propagated, though extra interpreter arguments should be supplied.
+ return new Pair<>(method, extraInterpreterArgs(method, null, getLocation(), environment));
+ } else {
+ argumentListConversionResult = convertArgumentList(args, kwargs, method, environment);
+
+ if (argumentListConversionResult.getArguments() != null) {
+ return new Pair<>(method, argumentListConversionResult.getArguments());
} else {
- argumentListConversionResult = convertArgumentList(args, kwargs, method, environment);
- if (argumentListConversionResult.getArguments() != null) {
- if (matchingMethod == null) {
- matchingMethod = new Pair<>(method, argumentListConversionResult.getArguments());
- } else {
- throw new EvalException(
- getLocation(),
- String.format(
- "type '%s' has multiple matches for function %s",
- EvalUtils.getDataTypeNameFromClass(objClass),
- formatMethod(objClass, methodName, args, kwargs)));
- }
- }
+ throw new EvalException(getLocation(),
+ String.format(
+ "%s, in method call %s of '%s'",
+ argumentListConversionResult.getError(),
+ formatMethod(objClass, methodName, args, kwargs),
+ EvalUtils.getDataTypeNameFromClass(objClass)));
}
}
+ } else { // method == null
+ throw new EvalException(getLocation(),
+ String.format(
+ "type '%s' has no method %s",
+ EvalUtils.getDataTypeNameFromClass(objClass),
+ formatMethod(objClass, methodName, args, kwargs)));
}
- if (matchingMethod == null) {
- String errorMessage;
- if (ClassObject.class.isAssignableFrom(objClass)) {
- errorMessage = String.format("struct has no method '%s'", methodName);
- } else if (argumentListConversionResult == null
- || argumentListConversionResult.getError() == null) {
- errorMessage =
- String.format(
- "type '%s' has no method %s",
- EvalUtils.getDataTypeNameFromClass(objClass),
- formatMethod(objClass, methodName, args, kwargs));
-
- } else {
- errorMessage =
- String.format(
- "%s, in method call %s of '%s'",
- argumentListConversionResult.getError(),
- formatMethod(objClass, methodName, args, kwargs),
- EvalUtils.getDataTypeNameFromClass(objClass));
- }
- throw new EvalException(getLocation(), errorMessage);
- }
- return matchingMethod;
}
/**
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
index c0374df..f904c42 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
@@ -2042,7 +2042,7 @@
checkError(
"test/skylark",
"r",
- "struct has no method 'output_group'",
+ "type 'Target' has no method output_group()",
"load('//test/skylark:extension.bzl', 'my_rule')",
"cc_binary(name = 'lib', data = ['a.txt'])",
"my_rule(name='r', dep = ':lib')");
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
index 0c12754..3337f07 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
@@ -1109,7 +1109,7 @@
@Test
public void testStructAccessingUnknownFieldWithArgs() throws Exception {
checkErrorContains(
- "struct has no method 'c'", "x = struct(a = 1, b = 2)", "y = x.c()");
+ "type 'struct' has no method c()", "x = struct(a = 1, b = 2)", "y = x.c()");
}
@Test
diff --git a/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessorTest.java b/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessorTest.java
index f42b695..0df865c 100644
--- a/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/SkylarkCallableProcessorTest.java
@@ -305,4 +305,14 @@
"Only one of @SkylarkCallable.enablingFlag and @SkylarkCallable.disablingFlag may be "
+ "specified.");
}
+
+ @Test
+ public void testConflictingMethodNames() throws Exception {
+ assertAbout(javaSource())
+ .that(getFile("ConflictingMethodNames.java"))
+ .processedWith(new SkylarkCallableProcessor())
+ .failsToCompile()
+ .withErrorContaining("Containing class has more than one method with name "
+ + "'conflicting_method' defined");
+ }
}
diff --git a/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/testsources/ConflictingMethodNames.java b/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/testsources/ConflictingMethodNames.java
new file mode 100644
index 0000000..e721402
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skylarkinterface/processor/testsources/ConflictingMethodNames.java
@@ -0,0 +1,46 @@
+// 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.skylarkinterface.processor.testsources;
+
+import com.google.devtools.build.lib.skylarkinterface.Param;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
+
+/**
+ * Test case for a class which contains multiple SkylarkCallable methods with the same name.
+ * This should cause a compile failure -- overrides are not allowed.
+ */
+public class ConflictingMethodNames {
+
+ @SkylarkCallable(
+ name = "conflicting_method",
+ documented = false,
+ parameters = {
+ @Param(name = "one", type = String.class, named = true),
+ })
+ public String conflictingMethod(String one) {
+ return "foo";
+ }
+
+ @SkylarkCallable(
+ name = "conflicting_method",
+ documented = false,
+ parameters = {
+ @Param(name = "one", type = String.class, named = true),
+ @Param(name = "two", type = Integer.class, named = true),
+ })
+ public String conflictingMethodTwo(String one, Integer two) {
+ return "foo";
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index 071019e..d733836 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -2218,7 +2218,8 @@
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.testIfExactError(
// TODO(bazel-team): This should probably match the error above better.
- "struct has no method 'nonexistent_method'", "v = val.nonexistent_method()");
+ "type 'SkylarkClassObjectWithSkylarkCallables' has no method nonexistent_method()",
+ "v = val.nonexistent_method()");
}
@Test