bazel syntax: simplify cache of information about annotated classes

This change combines three separate caches into one, which is
simpler, more compact, and cheaper to compute and (in some cases) access.

Also:
- eliminate hasSelfCallMethod.
- collectSkylarkMethodsWithAnnotation now returns methods in (Java) name
  order, not the rather arbitrary Method.toString order, inadvertently
  fixing a bug in Starlark completion.
PiperOrigin-RevId: 277416070
diff --git a/src/main/java/com/google/devtools/build/docgen/ApiExporter.java b/src/main/java/com/google/devtools/build/docgen/ApiExporter.java
index 48ad93d..c9743ba 100644
--- a/src/main/java/com/google/devtools/build/docgen/ApiExporter.java
+++ b/src/main/java/com/google/devtools/build/docgen/ApiExporter.java
@@ -117,10 +117,11 @@
       } else {
         SkylarkModule typeModule = SkylarkInterfaceUtils.getSkylarkModule(obj.getClass());
         if (typeModule != null) {
-          if (CallUtils.hasSelfCallMethod(StarlarkSemantics.DEFAULT_SEMANTICS, obj.getClass())) {
-            MethodDescriptor descriptor =
-                CallUtils.getSelfCallMethodDescriptor(StarlarkSemantics.DEFAULT_SEMANTICS, obj);
-            value = valueFromMethodDescriptor(descriptor);
+          MethodDescriptor selfCall =
+              CallUtils.getSelfCallMethodDescriptor(
+                  StarlarkSemantics.DEFAULT_SEMANTICS, obj.getClass());
+          if (selfCall != null) {
+            value = valueFromMethodDescriptor(selfCall);
           } else {
             value.setName(entry.getKey());
             value.setType(entry.getKey());
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java
index 459247f..c29a393 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java
@@ -23,7 +23,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.collect.compacthashset.CompactHashSet;
 import com.google.devtools.build.lib.events.Location;
@@ -33,18 +32,16 @@
 import com.google.devtools.build.lib.syntax.Runtime.NoneType;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics.FlagIdentifier;
+import com.google.devtools.build.lib.util.Pair;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /** Helper functions for implementing function calls. */
@@ -55,180 +52,107 @@
 
   private CallUtils() {} // uninstantiable
 
-  /**
-   * Cache key for callable method lookup of skylark types. The key consists of the class of the
-   * skylark type, and a skylark semantics object. The semantics object is required as part of the
-   * key as certain methods of the class may be unavailable if certain semantics flags are flipped.
-   */
-  private static final class MethodDescriptorKey {
-    private final Class<?> clazz;
-    private final StarlarkSemantics semantics;
-
-    private MethodDescriptorKey(Class<?> clazz, StarlarkSemantics semantics) {
-      this.clazz = clazz;
-      this.semantics = semantics;
+  private static CacheValue getCacheValue(Class<?> cls, StarlarkSemantics semantics) {
+    if (cls == String.class) {
+      cls = StringModule.class;
     }
-
-    Class<?> getClazz() {
-      return clazz;
-    }
-
-    StarlarkSemantics getSemantics() {
-      return semantics;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      MethodDescriptorKey that = (MethodDescriptorKey) o;
-      return Objects.equals(clazz, that.clazz) && Objects.equals(semantics, that.semantics);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(clazz, semantics);
+    try {
+      return cache.get(Pair.of(cls, semantics));
+    } catch (ExecutionException ex) {
+      throw new IllegalStateException("cache error", ex);
     }
   }
 
-  private static final LoadingCache<MethodDescriptorKey, Optional<MethodDescriptor>> selfCallCache =
+  // Information derived from a SkylarkCallable-annotated class and a StarlarkSemantics.
+  private static class CacheValue {
+    @Nullable MethodDescriptor selfCall;
+    ImmutableMap<String, MethodDescriptor> fields; // sorted by Java method name
+    ImmutableMap<String, MethodDescriptor> methods; // sorted by Java method name
+  }
+
+  // A cache of information derived from a SkylarkCallable-annotated class and a StarlarkSemantics.
+  private static final LoadingCache<Pair<Class<?>, StarlarkSemantics>, CacheValue> cache =
       CacheBuilder.newBuilder()
           .build(
-              new CacheLoader<MethodDescriptorKey, Optional<MethodDescriptor>>() {
+              new CacheLoader<Pair<Class<?>, StarlarkSemantics>, CacheValue>() {
                 @Override
-                public Optional<MethodDescriptor> load(MethodDescriptorKey key) throws Exception {
-                  Class<?> keyClass = key.getClazz();
-                  StarlarkSemantics semantics = key.getSemantics();
-                  MethodDescriptor returnValue = null;
-                  for (Method method : sortMethodArrayByMethodName(keyClass.getMethods())) {
+                public CacheValue load(Pair<Class<?>, StarlarkSemantics> key) throws Exception {
+                  Class<?> cls = key.first;
+                  StarlarkSemantics semantics = key.second;
+
+                  MethodDescriptor selfCall = null;
+                  ImmutableMap.Builder<String, MethodDescriptor> methods = ImmutableMap.builder();
+                  Map<String, MethodDescriptor> fields = new HashMap<>();
+
+                  // Sort methods by Java name, for determinism.
+                  Method[] classMethods = cls.getMethods();
+                  Arrays.sort(classMethods, Comparator.comparing(Method::getName));
+                  for (Method method : classMethods) {
                     // Synthetic methods lead to false multiple matches
                     if (method.isSynthetic()) {
                       continue;
                     }
+
+                    // annotated?
                     SkylarkCallable callable = SkylarkInterfaceUtils.getSkylarkCallable(method);
-                    if (callable != null && callable.selfCall()) {
-                      if (returnValue != null) {
+                    if (callable == null) {
+                      continue;
+                    }
+
+                    // enabled by semantics?
+                    if (!semantics.isFeatureEnabledBasedOnTogglingFlags(
+                        callable.enableOnlyWithFlag(), callable.disableWithFlag())) {
+                      continue;
+                    }
+
+                    MethodDescriptor descriptor = MethodDescriptor.of(method, callable, semantics);
+
+                    // self-call method?
+                    if (callable.selfCall()) {
+                      if (selfCall != null) {
                         throw new IllegalArgumentException(
                             String.format(
-                                "Class %s has two selfCall methods defined", keyClass.getName()));
+                                "Class %s has two selfCall methods defined", cls.getName()));
                       }
-                      if (semantics.isFeatureEnabledBasedOnTogglingFlags(
-                          callable.enableOnlyWithFlag(), callable.disableWithFlag())) {
-                        returnValue = MethodDescriptor.of(method, callable, semantics);
-                      }
+                      selfCall = descriptor;
+                      continue;
                     }
-                  }
-                  return Optional.ofNullable(returnValue);
-                }
-              });
 
-  private static final LoadingCache<MethodDescriptorKey, Map<String, MethodDescriptor>>
-      methodCache =
-          CacheBuilder.newBuilder()
-              .build(
-                  new CacheLoader<MethodDescriptorKey, Map<String, MethodDescriptor>>() {
+                    // regular method
+                    methods.put(callable.name(), descriptor);
 
-                    @Override
-                    public Map<String, MethodDescriptor> load(MethodDescriptorKey key)
-                        throws Exception {
-                      Class<?> keyClass = key.getClazz();
-                      StarlarkSemantics semantics = key.getSemantics();
-                      ImmutableMap.Builder<String, MethodDescriptor> methodMap =
-                          ImmutableMap.builder();
-                      for (Method method : sortMethodArrayByMethodName(keyClass.getMethods())) {
-                        // Synthetic methods lead to false multiple matches
-                        if (method.isSynthetic()) {
-                          continue;
-                        }
-                        SkylarkCallable callable = SkylarkInterfaceUtils.getSkylarkCallable(method);
-                        if (callable == null) {
-                          continue;
-                        }
-                        if (callable.selfCall()) {
-                          // Self-call java methods are not treated as methods of the skylark value.
-                          continue;
-                        }
-                        if (semantics.isFeatureEnabledBasedOnTogglingFlags(
-                            callable.enableOnlyWithFlag(), callable.disableWithFlag())) {
-                          methodMap.put(
-                              callable.name(), MethodDescriptor.of(method, callable, semantics));
-                        }
-                      }
-                      return methodMap.build();
-                    }
-                  });
-
-  private static final LoadingCache<MethodDescriptorKey, Map<String, MethodDescriptor>> fieldCache =
-      CacheBuilder.newBuilder()
-          .build(
-              new CacheLoader<MethodDescriptorKey, Map<String, MethodDescriptor>>() {
-
-                @Override
-                public Map<String, MethodDescriptor> load(MethodDescriptorKey key)
-                    throws Exception {
-                  ImmutableMap.Builder<String, MethodDescriptor> fieldMap = ImmutableMap.builder();
-                  HashSet<String> fieldNamesForCollisions = new HashSet<>();
-                  List<MethodDescriptor> fieldMethods =
-                      methodCache.get(key).values().stream()
-                          .filter(MethodDescriptor::isStructField)
-                          .collect(Collectors.toList());
-
-                  for (MethodDescriptor fieldMethod : fieldMethods) {
-                    String name = fieldMethod.getName();
-                    // TODO(b/72113542): Validate with annotation processor instead of at runtime.
-                    if (!fieldNamesForCollisions.add(name)) {
+                    // field method?
+                    if (descriptor.isStructField()
+                        && fields.put(callable.name(), descriptor) != null) {
+                      // TODO(b/72113542): Validate with annotation processor instead of at runtime.
                       throw new IllegalArgumentException(
                           String.format(
-                              "Class %s has two structField methods named %s defined",
-                              key.getClazz().getName(), name));
+                              "Class %s declares two structField methods named %s",
+                              cls.getName(), callable.name()));
                     }
-                    fieldMap.put(name, fieldMethod);
                   }
-                  return fieldMap.build();
+
+                  CacheValue value = new CacheValue();
+                  value.selfCall = selfCall;
+                  value.methods = methods.build();
+                  value.fields = ImmutableMap.copyOf(fields);
+                  return value;
                 }
               });
 
-  // *args, **kwargs, location, ast, thread, skylark semantics
-  private static final int EXTRA_ARGS_COUNT = 6;
-
   /**
    * Returns a map of methods and corresponding SkylarkCallable annotations of the methods of the
-   * classObj class reachable from Skylark.
+   * objClass class reachable from Skylark. Elements are sorted by Java method name (which is not
+   * necessarily the same as Starlark attribute name).
    */
   public static ImmutableMap<Method, SkylarkCallable> collectSkylarkMethodsWithAnnotation(
-      Class<?> classObj) {
-    ImmutableSortedMap.Builder<Method, SkylarkCallable> methodMap
-        = ImmutableSortedMap.orderedBy(Comparator.comparing(Object::toString));
-    for (Method method : sortMethodArrayByMethodName(classObj.getMethods())) {
-      // Synthetic methods lead to false multiple matches
-      if (!method.isSynthetic()) {
-        SkylarkCallable annotation = SkylarkInterfaceUtils.getSkylarkCallable(classObj, method);
-        if (annotation != null) {
-          methodMap.put(method, annotation);
-        }
-      }
+      Class<?> objClass) {
+    ImmutableMap.Builder<Method, SkylarkCallable> result = ImmutableMap.builder();
+    for (MethodDescriptor desc :
+        getCacheValue(objClass, StarlarkSemantics.DEFAULT_SEMANTICS).methods.values()) {
+      result.put(desc.getMethod(), desc.getAnnotation());
     }
-    return methodMap.build();
-  }
-
-  /** Sort Method arrays by their name for a deterministic ordering */
-  private static Method[] sortMethodArrayByMethodName(Method[] methods) {
-    Arrays.sort(methods, Comparator.comparing(Method::getName));
-    return methods;
-  }
-
-  /**
-   * Returns either the class itself or, if the class is {@link String}, the proxy class
-   * containing all 'string' methods.
-   */
-  private static Class<?> getClassOrProxyClass(Class<?> clazz) {
-    return String.class.isAssignableFrom(clazz)
-        ? StringModule.class
-        : clazz;
+    return result.build();
   }
 
   /**
@@ -243,14 +167,8 @@
 
   /** Returns the Skylark callable Method of objClass with structField=true and the given name. */
   public static MethodDescriptor getStructField(
-      StarlarkSemantics semantics, Class<?> objClass, String methodName) {
-    try {
-      return fieldCache
-          .get(new MethodDescriptorKey(getClassOrProxyClass(objClass), semantics))
-          .get(methodName);
-    } catch (ExecutionException e) {
-      throw new IllegalStateException("Method loading failed: " + e);
-    }
+      StarlarkSemantics semantics, Class<?> objClass, String name) {
+    return getCacheValue(objClass, semantics).fields.get(name);
   }
 
   /**
@@ -265,13 +183,7 @@
 
   /** Returns the list of names of Skylark callable Methods of objClass with structField=true. */
   public static Set<String> getStructFieldNames(StarlarkSemantics semantics, Class<?> objClass) {
-    try {
-      return fieldCache
-          .get(new MethodDescriptorKey(getClassOrProxyClass(objClass), semantics))
-          .keySet();
-    } catch (ExecutionException e) {
-      throw new IllegalStateException("Method loading failed: " + e);
-    }
+    return getCacheValue(objClass, semantics).fields.keySet();
   }
 
   /**
@@ -287,13 +199,7 @@
   /** Returns the list of Skylark callable Methods of objClass with the given name. */
   public static MethodDescriptor getMethod(
       StarlarkSemantics semantics, Class<?> objClass, String methodName) {
-    try {
-      return methodCache
-          .get(new MethodDescriptorKey(getClassOrProxyClass(objClass), semantics))
-          .get(methodName);
-    } catch (ExecutionException e) {
-      throw new IllegalStateException("Method loading failed: " + e);
-    }
+    return getCacheValue(objClass, semantics).methods.get(methodName);
   }
 
   /**
@@ -312,52 +218,26 @@
    * objClass}.
    */
   public static Set<String> getMethodNames(StarlarkSemantics semantics, Class<?> objClass) {
-    try {
-      return methodCache
-          .get(new MethodDescriptorKey(getClassOrProxyClass(objClass), semantics))
-          .keySet();
-    } catch (ExecutionException e) {
-      throw new IllegalStateException("Method loading failed: " + e);
-    }
-  }
-
-  /**
-   * Returns true if the given class has a method annotated with {@link SkylarkCallable} with {@link
-   * SkylarkCallable#selfCall()} set to true.
-   */
-  public static boolean hasSelfCallMethod(StarlarkSemantics semantics, Class<?> objClass) {
-    try {
-      return selfCallCache.get(new MethodDescriptorKey(objClass, semantics)).isPresent();
-    } catch (ExecutionException e) {
-      throw new IllegalStateException("Method loading failed: " + e);
-    }
+    return getCacheValue(objClass, semantics).methods.keySet();
   }
 
   /**
    * Returns a {@link MethodDescriptor} 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
+   * SkylarkCallable#selfCall()} set to true). Returns null if no such method exists.
    */
+  @Nullable
   public static MethodDescriptor getSelfCallMethodDescriptor(
-      StarlarkSemantics semantics, Object obj) {
-    try {
-      Optional<MethodDescriptor> selfCallDescriptor =
-          selfCallCache.get(new MethodDescriptorKey(obj.getClass(), semantics));
-      if (!selfCallDescriptor.isPresent()) {
-        throw new IllegalStateException("Class " + obj.getClass() + " has no selfCall method");
-      }
-      return selfCallDescriptor.get();
-    } catch (ExecutionException e) {
-      throw new IllegalStateException("Method loading failed: " + e);
-    }
+      StarlarkSemantics semantics, Class<?> objClass) {
+    return getCacheValue(objClass, semantics).selfCall;
   }
 
   /**
    * Returns a {@link BuiltinCallable} representing a {@link SkylarkCallable}-annotated instance
-   * method of a given object with the given method name.
+   * method of a given object with the given Java method name.
    */
+  // TODO(adonovan): replace with EvalUtils.getAttr, once the latter doesn't require
+  // a Thread and Location.
   public static BuiltinCallable getBuiltinCallable(Object obj, String methodName) {
     Class<?> objClass = obj.getClass();
     MethodDescriptor methodDescriptor = getMethod(objClass, methodName);
@@ -421,7 +301,9 @@
         "struct field methods should be handled by DotExpression separately");
 
     ImmutableList<ParamDescriptor> parameters = method.getParameters();
-    List<Object> builder = new ArrayList<>(parameters.size() + EXTRA_ARGS_COUNT);
+    // *args, **kwargs, location, ast, thread, skylark semantics
+    final int extraArgsCount = 6;
+    List<Object> builder = new ArrayList<>(parameters.size() + extraArgsCount);
     boolean acceptsExtraArgs = method.isAcceptsExtraArgs();
     boolean acceptsExtraKwargs = method.isAcceptsExtraKwargs();
 
@@ -807,16 +689,18 @@
     if (fn instanceof StarlarkCallable) {
       StarlarkCallable callable = (StarlarkCallable) fn;
       return callable.call(posargs, ImmutableMap.copyOf(kwargs), call, thread);
-    } else if (hasSelfCallMethod(thread.getSemantics(), fn.getClass())) {
-      MethodDescriptor descriptor = getSelfCallMethodDescriptor(thread.getSemantics(), fn);
+    }
+
+    MethodDescriptor selfCall = getSelfCallMethodDescriptor(thread.getSemantics(), fn.getClass());
+    if (selfCall != null) {
       Object[] javaArguments =
           convertStarlarkArgumentsToJavaMethodArguments(
-              thread, call, descriptor, fn.getClass(), posargs, kwargs);
-      return descriptor.call(fn, javaArguments, call.getLocation(), thread);
-    } else {
-      throw new EvalException(
-          call.getLocation(), "'" + EvalUtils.getDataTypeName(fn) + "' object is not callable");
+              thread, call, selfCall, fn.getClass(), posargs, kwargs);
+      return selfCall.call(fn, javaArguments, call.getLocation(), thread);
     }
+
+    throw new EvalException(
+        call.getLocation(), "'" + EvalUtils.getDataTypeName(fn) + "' object is not callable");
   }
 
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
index ee5707e..253375a 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
@@ -175,6 +175,10 @@
     return name;
   }
 
+  Method getMethod() {
+    return method;
+  }
+
   /** @see SkylarkCallable#structField() */
   public boolean isStructField() {
     return structField;