// 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.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.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.SkylarkValue; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import com.google.devtools.build.lib.util.Preconditions; import java.util.ArrayList; import java.util.Collection; import java.util.List; 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, // TODO(bazel-team): Move this documentation to a dedicated page and link to it from here. doc = "A language built-in type that supports efficiently accumulating data from transitive " + "dependencies. Note that depsets are not hash sets: they don't support fast membership " + "tests, but on the contrary they support fast union. Depsets are designed to be used as " + "a collection of items (such as file names) generated by Bazel targets. " + "Depsets can be created using the depset function, " + "and they support the | operator to extend the depset with more elements or " + "to nest other depsets inside of it. Examples:
" + "

s = depset([1, 2])\n"
        + "s = s | [3]              # s == {1, 2, 3}\n"
        + "s = s | depset([4, 5])   # s == {1, 2, 3, {4, 5}}\n"
        + "other = depset([\"a\", \"b\", \"c\"], order=\"postorder\")
" + "Note that in these examples {..} is not a valid literal to create depsets. " + "Depsets have a fixed generic type, so depset([1]) + [\"a\"] or " + "depset([1]) + depset([\"a\"]) results in an error.
" + "Elements in a depset can neither be mutable nor be of type list, " + "struct or dict.
" + "When aggregating data from providers, depsets can take significantly less memory than " + "other types as they support nesting, that is, their subsets are shared in memory.
" + "Every depset has an order parameter which determines the iteration order. " + "There are four possible values:" + "" + "Except for default, the above values are incompatible with each other. " + "Consequently, two depsets can only be merged via the + operator or via " + "union() if either both depsets have the same order or one of " + "the depsets has stable order. In the latter case the iteration order will " + "be determined by the outer depset, thus ignoring the order parameter of " + "nested depsets." ) @Immutable public final class SkylarkNestedSet implements SkylarkValue, SkylarkQueryable { private final SkylarkType contentType; private final NestedSet set; @Nullable private final List items; @Nullable private final List transitiveItems; // Dummy class used to create a documentation for the deprecated `set` type // TODO(bazel-team): remove before the end of 2017 @SkylarkModule( name = "set", category = SkylarkModuleCategory.BUILTIN, doc = "A deprecated alias for depset. " + "Please use depset instead. " + "If you need a hash set that supports O(1) membership testing " + "consider using a dict." ) static final class LegacySet { private LegacySet() {} } public SkylarkNestedSet(Order order, Object item, Location loc) throws EvalException { this(order, SkylarkType.TOP, item, loc, null); } public SkylarkNestedSet(SkylarkNestedSet left, Object right, Location loc) throws EvalException { this(left.set.getOrder(), left.contentType, right, loc, left); } // This is safe because of the type checking @SuppressWarnings("unchecked") private SkylarkNestedSet(Order order, SkylarkType contentType, Object item, Location loc, @Nullable SkylarkNestedSet left) throws EvalException { ArrayList items = new ArrayList<>(); ArrayList transitiveItems = new ArrayList<>(); if (left != null) { if (left.items == null) { // SkylarkSet created from native NestedSet transitiveItems.add(left.set); } else { // Preserving the left-to-right addition order. items.addAll(left.items); transitiveItems.addAll(left.transitiveItems); } } // Adding the item if (item instanceof SkylarkNestedSet) { SkylarkNestedSet nestedSet = (SkylarkNestedSet) item; if (!nestedSet.isEmpty()) { contentType = getTypeAfterInsert(contentType, nestedSet.contentType, loc); transitiveItems.add(nestedSet.set); } } else if (item instanceof SkylarkList) { // TODO(bazel-team): we should check ImmutableList here but it screws up genrule at line 43 for (Object object : (SkylarkList) item) { contentType = getTypeAfterInsert(contentType, SkylarkType.of(object.getClass()), loc); checkImmutable(object, loc); items.add(object); } } else { throw new EvalException( loc, String.format( "cannot union value of type '%s' to a depset", EvalUtils.getDataTypeName(item))); } this.contentType = Preconditions.checkNotNull(contentType, "type cannot be null"); // Initializing the real nested set NestedSetBuilder builder = new NestedSetBuilder<>(order); builder.addAll(items); try { for (NestedSet nestedSet : transitiveItems) { builder.addTransitive(nestedSet); } } catch (IllegalStateException e) { throw new EvalException(loc, e.getMessage()); } this.set = builder.build(); this.items = ImmutableList.copyOf(items); this.transitiveItems = ImmutableList.copyOf(transitiveItems); } /** * 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); } /** * 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); } /** * A not type safe constructor for SkylarkNestedSet. It's discouraged to use it unless type * generic safety is guaranteed from the caller side. */ SkylarkNestedSet(SkylarkType contentType, NestedSet set) { // This is here for the sake of FuncallExpression. this.contentType = Preconditions.checkNotNull(contentType, "type cannot be null"); this.set = Preconditions.checkNotNull(set, "depset cannot be null"); this.items = null; this.transitiveItems = null; } /** * A not type safe constructor for SkylarkNestedSet, specifying type as a Java class. * It's discouraged to use it unless type generic safety is guaranteed from the caller side. */ public SkylarkNestedSet(Class contentType, NestedSet set) { this(SkylarkType.of(contentType), set); } private static final SkylarkType DICT_LIST_UNION = SkylarkType.Union.of(SkylarkType.DICT, SkylarkType.LIST); /** * Throws EvalException if a type overlaps with DICT or LIST. */ private static void checkTypeNotDictOrList(SkylarkType type, Location loc) throws EvalException { if (SkylarkType.intersection(DICT_LIST_UNION, type) != SkylarkType.BOTTOM) { throw new EvalException( loc, String.format("depsets cannot contain items of type '%s'", type)); } } /** * Returns the intersection of two types, and throws EvalException if the intersection is bottom. */ private static SkylarkType commonNonemptyType( SkylarkType depsetType, SkylarkType itemType, Location loc) throws EvalException { 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; } /** * 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, Location loc) throws EvalException { checkTypeNotDictOrList(itemType, loc); return commonNonemptyType(depsetType, itemType, loc); } /** * 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() { // Do not remove : workaround for Java 7 type inference. 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(); } @SkylarkCallable( name = "to_list", doc = "Returns a frozen list of the elements, without duplicates, in the depset's traversal " + "order.") public MutableList skylarkToList() { return new MutableList(set, null); } 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 write(Appendable buffer, char quotationMark) { Printer.append(buffer, "depset("); Printer.printList(buffer, set, "[", ", ", "]", null, quotationMark); Order order = getOrder(); if (order != Order.STABLE_ORDER) { Printer.append(buffer, ", order = \"" + order.getSkylarkName() + "\""); } Printer.append(buffer, ")"); } @Override public final boolean containsKey(Object key, Location loc) throws EvalException { return (set.toSet().contains(key)); } }