| // 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); | |
| } | |
| } | |
| } |