blob: e94382a62a902e5255467d0b74be52de07348e93 [file] [log] [blame]
// 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);
}
}
}