bazel syntax: enforce that ClassObject.getValue returns legal Starlark values

EvalUtils.getAttr now enforces that the value returned by
ClassObject.getValue is a legal value.
Previously, only the ternary SkylarkClassObject.getValue method
offered this postcondition, and it wasn't enforced.

This turned up BuildConfiguration.Fragment,
which was used as a value (e.g. in ctx.fragments.android)
but had forgotten to declare its relationship to StarlarkValue.

Also:
- remove obsolete 'instanceof Class' check.
- remove try/catch of IllegalArgumentException.
  Instead do this check with narrower scope in the fragment code.
  We will fix other cases as they appear.
PiperOrigin-RevId: 284644428
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index f167e8a..0810a03 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -475,18 +475,25 @@
   }
 
   @Nullable
-  public Fragment getSkylarkFragment(String name, ConfigurationTransition transition) {
+  public Fragment getSkylarkFragment(String name, ConfigurationTransition transition)
+      throws EvalException {
     Class<? extends Fragment> fragmentClass =
         getConfiguration(transition).getSkylarkFragmentByName(name);
     if (fragmentClass == null) {
       return null;
     }
-    return getFragment(fragmentClass, name,
-        String.format(
-            " Please update the '%1$sfragments' argument of the rule definition "
-            + "(for example: %1$sfragments = [\"%2$s\"])",
-            (transition.isHostTransition()) ? "host_" : "", name),
-        transition);
+    try {
+      return getFragment(
+          fragmentClass,
+          name,
+          String.format(
+              " Please update the '%1$sfragments' argument of the rule definition "
+                  + "(for example: %1$sfragments = [\"%2$s\"])",
+              transition.isHostTransition() ? "host_" : "", name),
+          transition);
+    } catch (IllegalArgumentException ex) { // fishy
+      throw new EvalException(null, ex.getMessage());
+    }
   }
 
   public ImmutableCollection<String> getSkylarkFragmentNames(ConfigurationTransition transition) {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index 9b07ff9..5fbd7bd 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -48,6 +48,7 @@
 import com.google.devtools.build.lib.skylarkbuildapi.BuildConfigurationApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import com.google.devtools.build.lib.syntax.StarlarkValue;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -110,8 +111,10 @@
    * <p>All implementations must be immutable and communicate this as clearly as possible (e.g.
    * declare {@link ImmutableList} signatures on their interfaces vs. {@link List}). This is because
    * fragment instances may be shared across configurations.
+   *
+   * <p>Fragments are Starlark values, as returned by {@code ctx.fragments.android}, for example.
    */
-  public abstract static class Fragment {
+  public abstract static class Fragment implements StarlarkValue {
     /**
      * Validates the options for this Fragment. Issues warnings for the use of deprecated options,
      * and warnings or errors for any option settings that conflict.
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentCollection.java
index d396bcc..11f2ab7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentCollection.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentCollection.java
@@ -19,6 +19,7 @@
 import com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.skylarkbuildapi.FragmentCollectionApi;
+import com.google.devtools.build.lib.syntax.EvalException;
 import javax.annotation.Nullable;
 
 /** Represents a collection of configuration fragments in Skylark. */
@@ -35,7 +36,7 @@
 
   @Override
   @Nullable
-  public Object getValue(String name) {
+  public Object getValue(String name) throws EvalException {
     return ruleContext.getSkylarkFragment(name, transition);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index 94c7aff..307e0fd 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -447,48 +447,37 @@
     Eval.setDebugger(dbg);
   }
 
-  /** Returns the named field or method of the specified object. */
-  static Object getAttr(StarlarkThread thread, Location loc, Object object, String name)
+  /** Returns the named field or method of value {@code x}, or null if not found. */
+  static Object getAttr(StarlarkThread thread, Location loc, Object x, String name)
       throws EvalException, InterruptedException {
-    MethodDescriptor method =
-        object instanceof Class<?>
-            ? CallUtils.getMethod(thread.getSemantics(), (Class<?>) object, name)
-            : CallUtils.getMethod(thread.getSemantics(), object.getClass(), name);
+    // @SkylarkCallable-annotated field?
+    MethodDescriptor method = CallUtils.getMethod(thread.getSemantics(), x.getClass(), name);
     if (method != null && method.isStructField()) {
       return method.call(
-          object,
+          x,
           CallUtils.extraInterpreterArgs(method, /*ast=*/ null, loc, thread).toArray(),
           loc,
           thread);
     }
 
-    if (object instanceof SkylarkClassObject) {
-      Object result = null;
-      try {
-        result = ((SkylarkClassObject) object).getValue(loc, thread.getSemantics(), name);
-      } catch (IllegalArgumentException ex) { // TODO(adonovan): why necessary?
-        throw new EvalException(loc, ex);
-      }
-      if (result != null) {
-        return result;
-      }
-
-    } else if (object instanceof ClassObject) {
-      Object result = null;
-      try {
-        result = ((ClassObject) object).getValue(name);
-      } catch (IllegalArgumentException ex) {
-        throw new EvalException(loc, ex);
-      }
-      // ClassObjects may have fields that are annotated with @SkylarkCallable.
-      // Since getValue() does not know about those, we cannot expect that result is a valid object.
-      if (result != null) {
-        return Starlark.fromJava(result, thread.mutability());
+    // user-defined field?
+    if (x instanceof ClassObject) {
+      // TODO(adonovan): get rid of SkylarkClassObject once
+      // --incompatible_disable_target_provider_fields goes away,
+      // then remove the thread and loc parameters and publish
+      // this method as Starlark.getattr.
+      Object field =
+          x instanceof SkylarkClassObject
+              ? ((SkylarkClassObject) x).getValue(loc, thread.getSemantics(), name)
+              : ((ClassObject) x).getValue(name);
+      if (field != null) {
+        return Starlark.checkValid(field);
       }
     }
 
+    // @SkylarkCallable-annotated method?
     if (method != null) {
-      return new BuiltinCallable(object, name);
+      return new BuiltinCallable(x, name);
     }
 
     return null;
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkClassObject.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkClassObject.java
index 1532afe..c71b7a2 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkClassObject.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkClassObject.java
@@ -16,30 +16,17 @@
 import com.google.devtools.build.lib.events.Location;
 import javax.annotation.Nullable;
 
-/**
- * A marker interface for a {@link ClassObject} whose {@link #getValue} always returns a
- * Skylark-friendly value, with no defensive conversion required.
- *
- * <p>An value is Skylark-friendly if its class (or a supertype) implements {@link
- * com.google.devtools.build.lib.syntax.StarlarkValue}, is annotated with {@link
- * com.google.devtools.build.lib.skylarkinterface.SkylarkModule}, or is a Skylark primitive like
- * {@link String}.
- *
- * <p>Ideally, this class should not be needed, and all {@link ClassObject}s should only expose
- * Skylark-friendly values.
- */
+/** A variant of ClassObject for implementations that require a StarlarkSemantics. */
 public interface SkylarkClassObject extends ClassObject {
 
   /**
    * Returns the value of the field with the given name, or null if the field does not exist.
    *
-   * @param loc the location in the current Starlark evaluation context
-   * @param starlarkSemantics the current starlark semantics, which may affect which fields are
-   *     available, or the semantics of the available fields
-   * @param name the name of the field to return the value of
-   * @throws EvalException if a user-visible error occurs (other than non-existent field).
+   * @param loc the location of the field access operation
+   * @param semantics the Starlark semantics, which determine the available fields
+   * @param name the name of the field to retrieve
+   * @throws EvalException if the field exists could not be retrieved
    */
   @Nullable
-  Object getValue(Location loc, StarlarkSemantics starlarkSemantics, String name)
-      throws EvalException;
+  Object getValue(Location loc, StarlarkSemantics semantics, String name) throws EvalException;
 }