bazelbuild/bazel · error · IllegalArgumentException

Expected one of %s and %s to be a subclass of the other

Error message

Expected one of %s and %s to be a subclass of the other

What it means

Starlark's annotation machinery resolves a class's Starlark type by finding the most-specific @StarlarkBuiltin-annotated ancestor. If two annotated ancestors are unrelated (neither assignable to the other), there is no unique answer and this IllegalArgumentException is thrown. The fix, documented in the comment, is to annotate the class itself so it has its own explicit type.

Source

Thrown at src/main/java/net/starlark/java/annot/StarlarkAnnotations.java:51

  private static Class<?> moreSpecific(Class<?> x, Class<?> y) {
    if (x == null) {
      return y;
    } else if (y == null) {
      return x;
    } else if (x.isAssignableFrom(y)) {
      return y;
    } else if (y.isAssignableFrom(x)) {
      return x;
    } else {
      // If this exception occurs, it indicates the following error scenario:
      //
      // Suppose class A is a subclass of both B and C, where B and C are annotated with
      // @StarlarkBuiltin annotations (and are thus considered "Starlark types"). If B is not a
      // subclass of C (nor vice versa), then it's impossible to resolve whether A is of type
      // B or if A is of type C. It's both! The way to resolve this is usually to have A be its own
      // type (annotated with @StarlarkBuiltin), and thus have the explicit type of A be
      // semantically "B and C".
      throw new IllegalArgumentException(
          String.format("Expected one of %s and %s to be a subclass of the other", x, y));
    }
  }

  /**
   * Information extracted by walking a class's ancestors' {@link StarlarkBuiltin} annotations.
   *
   * @param starlarkBuiltinAncestor the most-specified ancestor annotated with {@link
   *     StarlarkBuiltin}. (It is guaranteed that if two ancestors both define the annotation, one
   *     of them is a subtype of the other.)
   * @param assignableToStructType whether any {@code StarlarkBuiltin}-annotated ancestor has set
   *     {@link StarlarkBuiltin#isStructType} to true.
   */
  private record ClassInfo(Class<?> starlarkBuiltinAncestor, boolean assignableToStructType) {}

  // A map from a class to its ClassInfo.
  private static final ClassValue<ClassInfo> classInfos =
      new ClassValue<ClassInfo>() {

View on GitHub (pinned to e6e199d060)

Solutions

  1. Annotate the class itself with @StarlarkBuiltin so its type is explicit and ancestor resolution never has to choose.
  2. Alternatively break the diamond: make the class inherit from only one @StarlarkBuiltin-annotated ancestor.
  3. If both interfaces must remain, verify one is annotated as a struct/subtype relationship or restructure so one extends the other.
  4. Re-run the Starlark integration test that registers the class to confirm resolution succeeds.

Example fix

// before
class MyValue implements FooType, BarType {} // both annotated, unrelated

// after
@StarlarkBuiltin(name = "my_value", doc = "...")
class MyValue implements FooType, BarType {}
Defensive patterns

Strategy: validation

Validate before calling

// Detect the unrelated-ancestor diamond before registering the class
static boolean hasUniqueBuiltinAncestor(Class<?> c) {
  List<Class<?>> annotated = new ArrayList<>();
  for (Class<?> k = c; k != null; k = k.getSuperclass()) {
    if (k.isAnnotationPresent(StarlarkBuiltin.class)) annotated.add(k);
    for (Class<?> i : k.getInterfaces()) {
      if (i.isAnnotationPresent(StarlarkBuiltin.class) && !annotated.contains(i)) annotated.add(i);
    }
  }
  for (int a = 0; a < annotated.size(); a++)
    for (int b = a + 1; b < annotated.size(); b++)
      if (!annotated.get(a).isAssignableFrom(annotated.get(b))
          && !annotated.get(b).isAssignableFrom(annotated.get(a))) return false;
  return true;
}

Prevention

When it happens

Trigger: A Java class extends/implements two distinct @StarlarkBuiltin-annotated types (diamond inheritance of Starlark types) without itself being @StarlarkBuiltin-annotated; StarlarkAnnotations.getStarlarkBuiltinAncestor (or CallUtils descriptor building) is invoked on such a class.

Common situations: Making a Starlark value class implement an extra Starlark interface (e.g. adding Sequence or Comparable besides its builtin base); merging two type hierarchies during a refactor; registering the class in a Starlark module.

Related errors


AI-assisted analysis of bazelbuild/bazel@e6e199d060 (2026-08-14). Data as JSON: /api/errors/3471de941f6093bf. Report an issue: GitHub.