bazel syntax: relax depset element validity check to isImmutable

The nightly uncovered cases of depsets containing lists of strings,
which are Starlark-unhashable even when frozen.

Also document the two problems related to the check.

PiperOrigin-RevId: 282377809
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Depset.java b/src/main/java/com/google/devtools/build/lib/syntax/Depset.java
index 30f6251..a796816 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Depset.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Depset.java
@@ -128,7 +128,7 @@
       }
     } else if (item instanceof Sequence) {
       for (Object x : (Sequence) item) {
-        EvalUtils.checkHashable(x);
+        checkElement(x);
         SkylarkType xt = SkylarkType.of(x);
         contentType = checkType(contentType, xt);
         itemsBuilder.add(x);
@@ -155,6 +155,30 @@
     return new Depset(contentType, builder.build(), items, transitiveItems);
   }
 
+  private static void checkElement(Object x) throws EvalException {
+    // Historically the requirement for a depset element was isImmutable(x).
+    // However, this check is neither necessary not sufficient.
+    // It is unnecessary because elements need only be hashable,
+    // as with dicts, whose keys may be mutable so long as mutations
+    // don't affect the hash code. (Elements of a NestedSet must be
+    // hashable because a hash-based set is used to de-duplicate
+    // elements during iteration.)
+    // And it is insufficient because some values are immutable
+    // but not Starlark-hashable, such as frozen lists.
+    // NestedSet calls its hashCode method regardless.
+    //
+    // TODO(adonovan): use this check instead:
+    //   EvalUtils.checkHashable(x);
+    // and delete the StarlarkValue.isImmutable and EvalUtils.isImmutable.
+    // Unfortunately this is a breaking change because some users
+    // construct depsets whose elements contain lists of strings,
+    // which are Starlark-unhashable even if frozen.
+    // TODO(adonovan): also remove StarlarkList.hashCode.
+    if (!EvalUtils.isImmutable(x)) {
+      throw new EvalException(null, "depset elements must not be mutable values");
+    }
+  }
+
   // implementation of deprecated depset(x) constructor
   static Depset legacyOf(Order order, Object items) throws EvalException {
     // TODO(adonovan): rethink this API. TOP is a pessimistic type for item, and it's wrong
@@ -438,6 +462,8 @@
    * <p>Use this to construct typesafe Skylark nested sets (depsets). Encapsulates content type
    * checking logic.
    */
+  // TODO(adonovan): make this private. The sole external user in rules/cpp could
+  // instead call Depset.of(NestedSet.wrap(Order, elems)).
   public static final class Builder {
 
     private final Order order;
@@ -452,8 +478,17 @@
 
     /** Adds a direct element, checking that its type is equal to the elements already added. */
     public Builder addDirect(Object x) throws EvalException {
-      // In case of problems, see b/144992997 or github.com/bazelbuild/bazel/issues/10289.
-      EvalUtils.checkHashable(x);
+      // Historically, checkElement was called only by some depset constructors,
+      // but not this one, depset(direct=[x]).
+      // This was a regrettable oversight that allowed users to put
+      // mutable values into depsets, doubly so because we have just forced our
+      // users to migrate away from the legacy constructor which applied the check.
+      // We are currently discovering and fixing existing violations, for example
+      // marking the relevant Starlark types as immutable where appropriate
+      // (e.g. ConfiguredTarget), but if violations are too numerous we may need
+      // to suppress the checkElement call below and reintroduce it as a breaking change.
+      // See b/144992997 or github.com/bazelbuild/bazel/issues/10289.
+      checkElement(x);
 
       SkylarkType xt = SkylarkType.of(x);
       this.contentType = checkType(contentType, xt);
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 1043d7e3..15ce13a 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
@@ -113,6 +113,13 @@
   /** Throws EvalException if x is not hashable. */
   static void checkHashable(Object x) throws EvalException {
     if (!isHashable(x)) {
+      // This results in confusing errors such as "unhashable type: tuple".
+      // TODO(adonovan): ideally the error message would explain which
+      // element of, say, a tuple is unhashable. The only practical way
+      // to implement this is by implementing isHashable as a call to
+      // Object.hashCode within a try/catch, and requiring all
+      // unhashable Starlark values to throw a particular unchecked exception
+      // with a helpful error message.
       throw new EvalException(
           null, Starlark.format("unhashable type: '%s'", EvalUtils.getDataTypeName(x)));
     }
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 9d650c2..5a71e61 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
@@ -1265,12 +1265,6 @@
     assertThat(result.iterator().next()).isInstanceOf(StructImpl.class);
   }
 
-  @Test
-  public void testNsetBadMutableItem() throws Exception {
-    checkEvalErrorContains("unhashable type: 'tuple'", "depset([([],)])");
-    checkEvalErrorContains("unhashable type: 'struct'", "depset([struct(a=[])])");
-  }
-
   private static StructImpl makeStruct(String field, Object value) {
     return StructProvider.STRUCT.create(ImmutableMap.of(field, value), "no field '%'");
   }
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
index 340745f..5f2bb99 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleImplementationFunctionsTest.java
@@ -962,7 +962,8 @@
   @Test
   public void testNsetContainsList() throws Exception {
     setRuleContext(createRuleContext("//foo:foo"));
-    checkEvalErrorContains("unhashable type: 'list'", "depset([[ruleContext.files.srcs]])");
+    checkEvalErrorContains(
+        "depset elements must not be mutable values", "depset([[ruleContext.files.srcs]])");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java b/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java
index b8ada81..0530589 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/DepsetTest.java
@@ -579,6 +579,24 @@
   }
 
   @Test
+  public void testElementsMustBeImmutable() throws Exception {
+    // Test legacy and new constructors.
+
+    // mutable list: error
+    checkEvalError("depset elements must not be mutable values", "depset([[1,2,3]])");
+    checkEvalError("depset elements must not be mutable values", "depset(direct=[[1,2,3]])");
+
+    // struct containing mutable list: error
+    checkEvalError("depset elements must not be mutable values", "depset([struct(a=[])])");
+    checkEvalError("depset elements must not be mutable values", "depset(direct=[struct(a=[])])");
+
+    // frozen list: no error
+    update("x", StarlarkList.empty());
+    eval("depset([x])");
+    eval("depset(direct=[x])");
+  }
+
+  @Test
   public void testDepthExceedsLimitDuringIteration() throws Exception {
     NestedSet.setApplicationDepthLimit(2000);
     new SkylarkTest()