// Copyright 2014 Google Inc. 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.annotations.VisibleForTesting;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
/**
* A generic type safe NestedSet wrapper for Skylark.
*/
@SkylarkModule(name = "set",
doc = "A language built-in type that supports (nested) sets. "
+ "Sets can be created using the global set
function, and they "
+ "support the +
operator to extend the set with more elements or to nest "
+ "other sets inside of it. Examples: "
+ "
s = set([1, 2])\n"
+ "s += [3] # s == {1, 2, 3}\n"
+ "s += set([4, 5]) # s == {1, 2, 3, {4, 5}} "
+ "Note that in these examples {..}
is not a valid literal to create sets. "
+ "Sets have a fixed generic type, so set([1]) + [\"a\"]
or "
+ "set([1]) + set([\"a\"])
results in an error.")
@Immutable
public final class SkylarkNestedSet implements Iterable {
private final SkylarkType contentType;
@Nullable private final List items;
@Nullable private final List> transitiveItems;
private final NestedSet> set;
public SkylarkNestedSet(Order order, Object item, Location loc) throws EvalException {
this(order, SkylarkType.TOP, item, loc, new ArrayList(),
new ArrayList>());
}
public SkylarkNestedSet(SkylarkNestedSet left, Object right, Location loc) throws EvalException {
this(left.set.getOrder(), left.contentType, right, loc,
new ArrayList(checkItems(left.items, loc)),
new ArrayList>(checkItems(left.transitiveItems, loc)));
}
private static T checkItems(T items, Location loc) throws EvalException {
// SkylarkNestedSets created directly from ordinary NestedSets (those were created in a
// native rule) don't have directly accessible items and transitiveItems, so we cannot
// add more elements to them.
if (items == null) {
throw new EvalException(loc, "Cannot add more elements to this set. Sets created in "
+ "native rules cannot be left side operands of the + operator.");
}
return items;
}
// This is safe because of the type checking
@SuppressWarnings("unchecked")
private SkylarkNestedSet(Order order, SkylarkType contentType, Object item, Location loc,
List items, List> transitiveItems) throws EvalException {
// Adding the item
if (item instanceof SkylarkNestedSet) {
SkylarkNestedSet nestedSet = (SkylarkNestedSet) item;
if (!nestedSet.isEmpty()) {
contentType = checkType(contentType, nestedSet.contentType, loc);
transitiveItems.add((NestedSet) 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 = checkType(contentType, SkylarkType.of(object.getClass()), loc);
items.add(object);
}
} else {
throw new EvalException(loc,
String.format("cannot add '%s'-s to nested sets", 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, "set 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.
*/
SkylarkNestedSet(Class> contentType, NestedSet> set) {
this(SkylarkType.of(contentType), set);
}
private static SkylarkType checkType(SkylarkType builderType, SkylarkType itemType, Location loc)
throws EvalException {
if (SkylarkType.intersection(
SkylarkType.Union.of(SkylarkType.MAP, SkylarkType.LIST, SkylarkType.STRUCT),
itemType) != SkylarkType.BOTTOM) {
throw new EvalException(loc, String.format("nested set item is composite (type of %s)",
itemType));
}
if (!EvalUtils.isSkylarkImmutable(itemType.getType())) {
throw new EvalException(loc, String.format("nested set item is not immutable (type of %s)",
itemType));
}
SkylarkType newType = SkylarkType.intersection(builderType, itemType);
if (newType == SkylarkType.BOTTOM) {
throw new EvalException(loc, String.format(
"cannot add an item of type %s to a nested %s", itemType, builderType));
}
return newType;
}
/**
* Returns the NestedSet embedded in this SkylarkNestedSet if it is of the parameter type.
*/
// The precondition ensures generic type safety
@SuppressWarnings("unchecked")
public NestedSet getSet(Class type) {
// Empty sets don't need have to have a type since they don't have items
if (set.isEmpty()) {
return (NestedSet) set;
}
Preconditions.checkArgument(contentType.canBeCastTo(type),
String.format("Expected a set of %ss but got a set of %ss",
EvalUtils.getDataTypeNameFromClass(type),
contentType));
return (NestedSet) set;
}
// For some reason this cast is unsafe in Java
@SuppressWarnings("unchecked")
@Override
public Iterator iterator() {
return (Iterator) set.iterator();
}
public Collection toCollection() {
return ImmutableList.copyOf(set.toCollection());
}
public boolean isEmpty() {
return set.isEmpty();
}
@VisibleForTesting
public SkylarkType getContentType() {
return contentType;
}
@Override
public String toString() {
return EvalUtils.prettyPrintValue(this);
}
/**
* Parse the string as a set order.
*/
public static Order parseOrder(String s, Location loc) throws EvalException {
// Keep in sync with orderString
if (s == null || s.equals("stable")) {
return Order.STABLE_ORDER;
} else if (s.equals("compile")) {
return Order.COMPILE_ORDER;
} else if (s.equals("link")) {
return Order.LINK_ORDER;
} else if (s.equals("naive_link")) {
return Order.NAIVE_LINK_ORDER;
} else {
throw new EvalException(loc, "Invalid order: " + s);
}
}
/**
* Get the order as a string.
*/
public static String orderString(Order order) {
// Keep in sync with parseOrder
switch (order) {
case STABLE_ORDER: return "stable";
case COMPILE_ORDER: return "compile";
case LINK_ORDER: return "link";
case NAIVE_LINK_ORDER: return "naive_link";
default: throw new IllegalStateException("unknown order: " + order);
}
}
public Order getOrder() {
return set.getOrder();
}
}