// Copyright 2014 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.syntax; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; import com.google.devtools.build.lib.skylarkinterface.Param; import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.skylarkinterface.SkylarkValue; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import java.util.Collection; import javax.annotation.Nullable; /** * A generic, type-safe {@link NestedSet} wrapper for Skylark. * *

The content type of a {@code SkylarkNestedSet} is the intersection of the {@link SkylarkType} * of each of its elements. It is an error if this intersection is {@link SkylarkType#BOTTOM}. An * empty set has a content type of {@link SkylarkType#TOP}. * *

It is also an error if this type has a non-bottom intersection with {@link SkylarkType#DICT} * or {@link SkylarkType#LIST}, unless the set is empty. * *

TODO(bazel-team): Decide whether this restriction is still useful. */ @SkylarkModule( name = "depset", category = SkylarkModuleCategory.BUILTIN, doc = "

A specialized data structure that supports efficient merge operations and has a defined " + "traversal order. Commonly used for accumulating data from transitive dependencies in " + "rules and aspects. For more information see here." + "

" + "Depsets are not implemented as hash sets and do not support fast membership tests. If " + "you need a general set datatype, you can simulate one using a dictionary where all " + "keys map to True." + "

" + "Depsets are immutable. They should be created using their " + "constructor function and merged or augmented with " + "other depsets via the transitive argument. There are other deprecated " + "methods (| and + operators, union method) that " + "will eventually go away." + "

" + "The order parameter determines the kind of traversal that is done to " + "convert the depset to an iterable. There are four possible values:" + "

" + "

" + "Two depsets may only be merged if either both depsets have the same order, or one of " + "them has \"default\" order. In the latter case the resulting depset's " + "order will be the same as the other's order." + "

" + "Depsets may contain duplicate values but these will be suppressed when iterating " + "(using to_list()). Duplicates may interfere with the ordering semantics." ) @Immutable @AutoCodec public final class SkylarkNestedSet implements SkylarkValue, SkylarkQueryable { private final SkylarkType contentType; private final NestedSet set; @Nullable private final ImmutableList items; @Nullable private final ImmutableList> transitiveItems; @AutoCodec.VisibleForSerialization SkylarkNestedSet( SkylarkType contentType, NestedSet set, ImmutableList items, ImmutableList> transitiveItems) { this.contentType = Preconditions.checkNotNull(contentType, "type cannot be null"); this.set = set; this.items = items; this.transitiveItems = transitiveItems; } static SkylarkNestedSet of( Order order, SkylarkType contentType, Object item, Location loc, @Nullable SkylarkNestedSet left) throws EvalException { ImmutableList.Builder itemsBuilder = ImmutableList.builder(); ImmutableList.Builder> transitiveItemsBuilder = ImmutableList.builder(); if (left != null) { if (left.items == null) { // SkylarkSet created from native NestedSet transitiveItemsBuilder.add(left.set); } else { // Preserving the left-to-right addition order. itemsBuilder.addAll(left.items); transitiveItemsBuilder.addAll(left.transitiveItems); } } // Adding the item if (item instanceof SkylarkNestedSet) { SkylarkNestedSet nestedSet = (SkylarkNestedSet) item; if (!nestedSet.isEmpty()) { contentType = getTypeAfterInsert( contentType, nestedSet.contentType, /*lastInsertedType=*/ null, loc); transitiveItemsBuilder.add(nestedSet.set); } } else if (item instanceof SkylarkList) { SkylarkType lastInsertedType = null; // TODO(bazel-team): we should check ImmutableList here but it screws up genrule at line 43 for (Object object : (SkylarkList) item) { SkylarkType elemType = SkylarkType.of(object.getClass()); contentType = getTypeAfterInsert(contentType, elemType, lastInsertedType, loc); lastInsertedType = elemType; checkImmutable(object, loc); itemsBuilder.add(object); } } else { throw new EvalException( loc, String.format( "cannot union value of type '%s' to a depset", EvalUtils.getDataTypeName(item))); } ImmutableList items = itemsBuilder.build(); ImmutableList> transitiveItems = transitiveItemsBuilder.build(); // Initializing the real nested set NestedSetBuilder builder = new NestedSetBuilder<>(order); builder.addAll(items); try { for (NestedSet nestedSet : transitiveItems) { builder.addTransitive(nestedSet); } } catch (IllegalArgumentException e) { // Order mismatch between item and builder. throw new EvalException(loc, e.getMessage()); } return new SkylarkNestedSet(contentType, builder.build(), items, transitiveItems); } public static SkylarkNestedSet of(Order order, Object item, Location loc) throws EvalException { return of(order, SkylarkType.TOP, item, loc, null); } public static SkylarkNestedSet of(SkylarkNestedSet left, Object right, Location loc) throws EvalException { return of(left.set.getOrder(), left.contentType, right, loc, left); } /** * Returns a type safe SkylarkNestedSet. Use this instead of the constructor if possible. */ public static SkylarkNestedSet of(SkylarkType contentType, NestedSet set) { return new SkylarkNestedSet(contentType, set, null, null); } /** * Returns a type safe SkylarkNestedSet. Use this instead of the constructor if possible. */ public static SkylarkNestedSet of(Class contentType, NestedSet set) { return of(SkylarkType.of(contentType), set); } private static final SkylarkType DICT_LIST_UNION = SkylarkType.Union.of(SkylarkType.DICT, SkylarkType.LIST); /** * Checks that an item type is allowed in a given set type, and returns the type of a new depset * with that item inserted. */ private static SkylarkType getTypeAfterInsert( SkylarkType depsetType, SkylarkType itemType, SkylarkType lastInsertedType, Location loc) throws EvalException { if (lastInsertedType != null && lastInsertedType.equals(itemType)) { // Fast path, type shoudln't have changed, so no need to check. // TODO(bazel-team): Make skylark type checking less expensive. return depsetType; } // Check not dict or list. if (SkylarkType.intersection(DICT_LIST_UNION, itemType) != SkylarkType.BOTTOM) { throw new EvalException( loc, String.format("depsets cannot contain items of type '%s'", itemType)); } // Check compatible. SkylarkType resultType = SkylarkType.intersection(depsetType, itemType); if (resultType == SkylarkType.BOTTOM) { throw new EvalException( loc, String.format( "cannot add an item of type '%s' to a depset of '%s'", itemType, depsetType)); } return resultType; } /** * Throws EvalException if a given value is mutable. */ private static void checkImmutable(Object o, Location loc) throws EvalException { if (!EvalUtils.isImmutable(o)) { throw new EvalException(loc, "depsets cannot contain mutable items"); } } private void checkHasContentType(Class type) { // Empty sets should be SkylarkType.TOP anyway. if (!set.isEmpty()) { Preconditions.checkArgument( contentType.canBeCastTo(type), "Expected a depset of '%s' but got a depset of '%s'", EvalUtils.getDataTypeNameFromClass(type), contentType); } } /** * Returns the embedded {@link NestedSet}, while asserting that its elements all have the given * type. * *

If you do not specifically need the {@code NestedSet} and you are going to flatten it * anyway, prefer {@link #toCollection} to make your intent clear. * * @param type a {@link Class} representing the expected type of the contents * @return the {@code NestedSet}, with the appropriate generic type * @throws IllegalArgumentException if the type does not accurately describe all elements */ // The precondition ensures generic type safety. @SuppressWarnings("unchecked") public NestedSet getSet(Class type) { checkHasContentType(type); return (NestedSet) set; } /** * Returns the contents of the set as a {@link Collection}. */ public Collection toCollection() { return ImmutableList.copyOf(set.toCollection()); } /** * Returns the contents of the set as a {@link Collection}, asserting that the set type is * compatible with {@code T}. * * @param type a {@link Class} representing the expected type of the contents * @throws IllegalArgumentException if the type does not accurately describe all elements */ // The precondition ensures generic type safety. @SuppressWarnings("unchecked") public Collection toCollection(Class type) { checkHasContentType(type); return (Collection) toCollection(); } public boolean isEmpty() { return set.isEmpty(); } public SkylarkType getContentType() { return contentType; } @Override public String toString() { return Printer.repr(this); } public Order getOrder() { return set.getOrder(); } @Override public boolean isImmutable() { return true; } @Override public void repr(SkylarkPrinter printer) { printer.append("depset("); printer.printList(set, "[", ", ", "]", null); Order order = getOrder(); if (order != Order.STABLE_ORDER) { printer.append(", order = "); printer.repr(order.getSkylarkName()); } printer.append(")"); } @Override public final boolean containsKey(Object key, Location loc) throws EvalException { return (set.toList().contains(key)); } @SkylarkCallable( name = "union", doc = "(Deprecated) Returns a new depset that is the merge " + "of the given depset and new_elements. Use the " + "transitive constructor argument instead.", parameters = { @Param(name = "new_elements", type = Object.class, doc = "The elements to be added.") }, useLocation = true, useEnvironment = true ) public SkylarkNestedSet union(Object newElements, Location loc, Environment env) throws EvalException { if (env.getSemantics().incompatibleDepsetUnion()) { throw new EvalException( loc, "depset method `.union` has been removed. See " + "https://docs.bazel.build/versions/master/skylark/depsets.html for " + "recommendations. Use --incompatible_depset_union=false " + "to temporarily disable this check."); } // newElements' type is Object because of the polymorphism on unioning two // SkylarkNestedSets versus a set and another kind of iterable. // Can't use EvalUtils#toIterable since that would discard this information. return SkylarkNestedSet.of(this, newElements, loc); } @SkylarkCallable( name = "to_list", doc = "Returns a list of the elements, without duplicates, in the depset's traversal order. " + "Note that order is unspecified (but deterministic) for elements that were added " + "more than once to the depset. Order is also unspecified for \"default\"" + "-ordered depsets, and for elements of child depsets whose order differs " + "from that of the parent depset. The list is a copy; modifying it has no effect " + "on the depset and vice versa.", useEnvironment = true ) public MutableList toList(Environment env) { return MutableList.copyOf(env, this.toCollection()); } /** * Create a {@link Builder} with specified order. * *

The {@code Builder} will use {@code location} to report errors. */ public static Builder builder(Order order, Location location) { return new Builder(order, location); } /** * Builder for {@link SkylarkNestedSet}. * *

Use this to construct typesafe Skylark nested sets (depsets). * Encapsulates content type checking logic. */ public static final class Builder { private final Order order; private final NestedSetBuilder builder; /** Location for error messages */ private final Location location; private SkylarkType contentType = SkylarkType.TOP; private SkylarkType lastInsertedType = null; private Builder(Order order, Location location) { this.order = order; this.location = location; this.builder = new NestedSetBuilder<>(order); } /** * Add a direct element, checking its type to be compatible to already added * elements and transitive sets. */ public Builder addDirect(Object direct) throws EvalException { SkylarkType elemType = SkylarkType.of(direct.getClass()); contentType = getTypeAfterInsert(contentType, elemType, lastInsertedType, location); lastInsertedType = elemType; builder.add(direct); return this; } /** * Add a transitive set, checking its content type to be compatible to already added * elements and transitive sets. */ public Builder addTransitive(SkylarkNestedSet transitive) throws EvalException { if (transitive.isEmpty()) { return this; } contentType = getTypeAfterInsert( contentType, transitive.getContentType(), lastInsertedType, this.location); lastInsertedType = transitive.getContentType(); if (!order.isCompatible(transitive.getOrder())) { throw new EvalException(location, String.format("Order '%s' is incompatible with order '%s'", order.getSkylarkName(), transitive.getOrder().getSkylarkName())); } builder.addTransitive(transitive.getSet(Object.class)); return this; } public SkylarkNestedSet build() { return new SkylarkNestedSet(contentType, builder.build(), null, null); } } }