| // 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. |
| * |
| * <p>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}. |
| * |
| * <p>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. |
| * |
| * <p>TODO(bazel-team): Decide whether this restriction is still useful. |
| */ |
| @SkylarkModule( |
| name = "depset", |
| category = SkylarkModuleCategory.BUILTIN, |
| doc = |
| "<p>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 <a href=\"../depsets.md\">here</a>." |
| + "<p>" |
| + "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 <code>True</code>." |
| + "<p>" |
| + "Depsets are immutable. They should be created using their " |
| + "<a href=\"globals.html#depset\">constructor function</a> and merged or augmented with " |
| + "other depsets via the <code>transitive</code> argument. There are other deprecated " |
| + "methods (<code>|</code> and <code>+</code> operators, <code>union</code> method) that " |
| + "will eventually go away." |
| + "<p>" |
| + "The <code>order</code> parameter determines the kind of traversal that is done to " |
| + "convert the depset to an iterable. There are four possible values:" |
| + "<ul>" |
| + "<li><code>\"default\"</code> (formerly <code>\"stable\"</code>): Order is unspecified " |
| + "(but deterministic).</li>" |
| + "<li><code>\"postorder\"</code> (formerly <code>\"compile\"</code>): A left-to-right " |
| + "post-ordering. Precisely, this recursively traverses all children leftmost-first, " |
| + "then the direct elements leftmost-first.</li>" |
| + "<li><code>\"preorder\"</code> (formerly <code>\"naive_link\"</code>): A left-to-right " |
| + "pre-ordering. Precisely, this traverses the direct elements leftmost-first, then " |
| + "recursively traverses the children leftmost-first.</li>" |
| + "<li><code>\"topological\"</code> (formerly <code>\"link\"</code>): A topological " |
| + "ordering from the root down to the leaves. There is no left-to-right guarantee.</li>" |
| + "</ul>" |
| + "<p>" |
| + "Two depsets may only be merged if either both depsets have the same order, or one of " |
| + "them has <code>\"default\"</code> order. In the latter case the resulting depset's " |
| + "order will be the same as the other's order." |
| + "<p>" |
| + "Depsets may contain duplicate values but these will be suppressed when iterating " |
| + "(using <code>to_list()</code>). 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<Object> items; |
| @Nullable private final ImmutableList<NestedSet<?>> transitiveItems; |
| |
| @AutoCodec.VisibleForSerialization |
| SkylarkNestedSet( |
| SkylarkType contentType, |
| NestedSet<?> set, |
| ImmutableList<Object> items, |
| ImmutableList<NestedSet<?>> 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<Object> itemsBuilder = ImmutableList.builder(); |
| ImmutableList.Builder<NestedSet<?>> 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); |
| 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<Object> items = itemsBuilder.build(); |
| ImmutableList<NestedSet<?>> transitiveItems = transitiveItemsBuilder.build(); |
| // Initializing the real nested set |
| NestedSetBuilder<Object> 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 <T> SkylarkNestedSet of(SkylarkType contentType, NestedSet<T> set) { |
| return new SkylarkNestedSet(contentType, set, null, null); |
| } |
| |
| /** |
| * Returns a type safe SkylarkNestedSet. Use this instead of the constructor if possible. |
| */ |
| public static <T> SkylarkNestedSet of(Class<T> contentType, NestedSet<T> 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 shouldn'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)); |
| } |
| |
| SkylarkType resultType = SkylarkType.intersection(depsetType, itemType); |
| |
| // New depset type should follow the following rules: |
| // 1. Only empty depsets may be of type TOP. |
| // 2. If the previous depset type fully contains the new item type, then the depset type is |
| // retained. |
| // 3. If the item type fully contains the old depset type, then the depset type becomes the |
| // item type. (The depset type becomes less strict.) |
| // 4. Otherwise, the insert is invalid. |
| if (depsetType == SkylarkType.TOP) { |
| return resultType; |
| } else if (resultType.equals(itemType)) { |
| return depsetType; |
| } else if (resultType.equals(depsetType)) { |
| return itemType; |
| } else { |
| throw new EvalException( |
| loc, |
| String.format( |
| "cannot add an item of type '%s' to a depset of '%s'", itemType, depsetType)); |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * <p>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 <T> NestedSet<T> getSet(Class<T> type) { |
| checkHasContentType(type); |
| return (NestedSet<T>) set; |
| } |
| |
| /** |
| * Returns the contents of the set as a {@link Collection}. |
| */ |
| public Collection<Object> 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 <T> Collection<T> toCollection(Class<T> type) { |
| checkHasContentType(type); |
| return (Collection<T>) 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 = |
| "<i>(Deprecated)</i> Returns a new <a href=\"depset.html\">depset</a> that is the merge " |
| + "of the given depset and <code>new_elements</code>. Use the " |
| + "<code>transitive</code> 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 <code>\"default\"" |
| + "</code>-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<Object> toList(Environment env) { |
| return MutableList.copyOf(env, this.toCollection()); |
| } |
| |
| /** |
| * Create a {@link Builder} with specified order. |
| * |
| * <p>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}. |
| * |
| * <p>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<Object> 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); |
| 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); |
| } |
| } |
| } |