// 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.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import com.google.devtools.build.lib.util.LoggingUtil; import com.google.devtools.build.lib.util.StringCanonicalizer; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import javax.annotation.Nullable; /** * <p>Root of Type symbol hierarchy for values in the build language.</p> * * <p>Type symbols are primarily used for their <code>convert</code> method, * which is a kind of cast operator enabling conversion from untyped (Object) * references to values in the build language, to typed references.</p> * * <p>For example, this code type-converts a value <code>x</code> returned by * the evaluator, to a list of strings:</p> * * <pre> * Object x = expr.eval(env); * List<String> s = Type.STRING_LIST.convert(x); * </pre> */ public abstract class Type<T> { protected Type() {} /** * Converts untyped Object x resulting from the evaluation of an expression in the build language, * into a typed object of type T. * * <p>x must be *directly* convertible to this type. This therefore disqualifies "selector * expressions" of the form "{ config1: 'value1_of_orig_type', config2: 'value2_of_orig_type; }" * (which support configurable attributes). To handle those expressions, see * {@link com.google.devtools.build.lib.packages.BuildType#selectableConvert}. * * @param x the build-interpreter value to convert. * @param what an object having a toString describing what x is for; should be included in * any exception thrown. Grammatically, must produce a string describe a syntactic * construct, e.g. "attribute 'srcs' of rule foo". * @param context the label of the current BUILD rule; must be non-null if resolution of * package-relative label strings is required * @throws ConversionException if there was a problem performing the type conversion */ public abstract T convert(Object x, Object what, @Nullable Object context) throws ConversionException; // TODO(bazel-team): Check external calls (e.g. in PackageFactory), verify they always want // this over selectableConvert. /** * Equivalent to {@link #convert(Object, String, Object)} where the label is {@code null}. * Useful for converting values to types that do not involve the type {@code LABEL} * and hence do not require the label of the current package. */ public final T convert(Object x, Object what) throws ConversionException { return convert(x, what, null); } /** * Like {@link #convert(Object, String, Object)}, but converts skylark {@code None} * to given {@code defaultValue}. */ @Nullable public final T convertOptional(Object x, String what, @Nullable Object context, T defaultValue) throws ConversionException { if (EvalUtils.isNullOrNone(x)) { return defaultValue; } return convert(x, what, context); } /** * Like {@link #convert(Object, String, Object)}, but converts skylark {@code None} * to java {@code null}. */ @Nullable public final T convertOptional(Object x, String what, @Nullable Object context) throws ConversionException { return convertOptional(x, what, context, null); } /** * Like {@link #convert(Object, String)}, but converts skylark {@code NONE} to java {@code null}. */ @Nullable public final T convertOptional(Object x, String what) throws ConversionException { return convertOptional(x, what, null); } public abstract T cast(Object value); @Override public abstract String toString(); /** * Returns the default value for this type; may return null iff no default is defined for this * type. */ public abstract T getDefaultValue(); /** * Function accepting a (potentially null) {@link Label} and an arbitrary context object. Used by * {@link #visitLabels}. */ public static interface LabelVisitor<C> { void visit(@Nullable Label label, @Nullable C context) throws InterruptedException; } /** * Invokes {@code visitor.visit(label, context)} for each {@link Label} {@code label} associated * with {@code value}, which is assumed an instance of this {@link Type}. * * <p>This is used to support reliable label visitation in * {@link com.google.devtools.build.lib.packages.AbstractAttributeMapper#visitLabels}. To preserve * that reliability, every type should faithfully define its own instance of this method. In other * words, be careful about defining default instances in base types that get auto-inherited by * their children. Keep all definitions as explicit as possible. */ public abstract <C> void visitLabels(LabelVisitor<C> visitor, Object value, @Nullable C context) throws InterruptedException; /** Classifications of labels by their usage. */ public enum LabelClass { /** Used for types which are not labels. */ NONE, /** Used for types which use labels to declare a dependency. */ DEPENDENCY, /** * Used for types which use labels to reference another target but do not declare a dependency, * in cases where doing so would cause a dependency cycle. */ NONDEP_REFERENCE, /** Used for types which use labels to declare an output path. */ OUTPUT, /** * Used for types which contain Fileset entries, which contain labels but do not produce * normal dependencies. */ FILESET_ENTRY } /** Returns the class of labels contained by this type, if any. */ public LabelClass getLabelClass() { return LabelClass.NONE; } /** * Implementation of concatenation for this type (e.g. "val1 + val2"). Returns null to * indicate concatenation isn't supported. */ public T concat(@SuppressWarnings("unused") Iterable<T> elements) { return null; } /** * Converts an initialized Type object into a tag set representation. * This operation is only valid for certain sub-Types which are guaranteed * to be properly initialized. * * @param value the actual value * @throws UnsupportedOperationException if the concrete type does not support * tag conversion or if a convertible type has no initialized value. */ public Set<String> toTagSet(Object value, String name) { String msg = "Attribute " + name + " does not support tag conversion."; throw new UnsupportedOperationException(msg); } /** * The type of an integer. */ public static final Type<Integer> INTEGER = new IntegerType(); /** * The type of a string. */ public static final Type<String> STRING = new StringType(); /** * The type of a boolean. */ public static final Type<Boolean> BOOLEAN = new BooleanType(); /** * The type of a list of not-yet-typed objects. */ public static final ObjectListType OBJECT_LIST = new ObjectListType(); /** * The type of a list of {@linkplain #STRING strings}. */ public static final ListType<String> STRING_LIST = ListType.create(STRING); /** * The type of a list of {@linkplain #INTEGER strings}. */ public static final ListType<Integer> INTEGER_LIST = ListType.create(INTEGER); /** * The type of a dictionary of {@linkplain #STRING strings}. */ public static final DictType<String, String> STRING_DICT = DictType.create(STRING, STRING); /** * The type of a dictionary of {@linkplain #STRING_LIST label lists}. */ public static final DictType<String, List<String>> STRING_LIST_DICT = DictType.create(STRING, STRING_LIST); /** * For ListType objects, returns the type of the elements of the list; for * all other types, returns null. (This non-obvious implementation strategy * is necessitated by the wildcard capture rules of the Java type system, * which disallow conversion from Type{List{ELEM}} to Type{List{?}}.) */ public Type<?> getListElementType() { return null; } /** * ConversionException is thrown when a type-conversion fails; it contains * an explanatory error message. */ public static class ConversionException extends EvalException { private static String message(Type<?> type, Object value, @Nullable Object what) { StringBuilder builder = new StringBuilder(); builder.append("expected value of type '").append(type).append("'"); if (what != null) { builder.append(" for ").append(what); } builder.append(", but got "); Printer.write(builder, value); builder.append(" (").append(EvalUtils.getDataTypeName(value)).append(")"); return builder.toString(); } public ConversionException(Type<?> type, Object value, @Nullable Object what) { super(null, message(type, value, what)); } public ConversionException(String message) { super(null, message); } } /******************************************************************** * * * Subclasses * * * ********************************************************************/ private static class ObjectType extends Type<Object> { @Override public Object cast(Object value) { return value; } @Override public String getDefaultValue() { throw new UnsupportedOperationException( "ObjectType has no default value"); } @Override public <T> void visitLabels(LabelVisitor<T> visitor, Object value, T context) { } @Override public String toString() { return "object"; } @Override public Object convert(Object x, Object what, Object context) { return x; } } private static class IntegerType extends Type<Integer> { @Override public Integer cast(Object value) { return (Integer) value; } @Override public Integer getDefaultValue() { return 0; } @Override public <T> void visitLabels(LabelVisitor<T> visitor, Object value, T context) { } @Override public String toString() { return "int"; } @Override public Integer convert(Object x, Object what, Object context) throws ConversionException { if (!(x instanceof Integer)) { throw new ConversionException(this, x, what); } return (Integer) x; } @Override public Integer concat(Iterable<Integer> elements) { int ans = 0; for (Integer elem : elements) { ans += elem; } return Integer.valueOf(ans); } } private static class BooleanType extends Type<Boolean> { @Override public Boolean cast(Object value) { return (Boolean) value; } @Override public Boolean getDefaultValue() { return false; } @Override public <T> void visitLabels(LabelVisitor<T> visitor, Object value, T context) { } @Override public String toString() { return "boolean"; } // Conversion to boolean must also tolerate integers of 0 and 1 only. @Override public Boolean convert(Object x, Object what, Object context) throws ConversionException { if (x instanceof Boolean) { return (Boolean) x; } Integer xAsInteger = INTEGER.convert(x, what, context); if (xAsInteger == 0) { return false; } else if (xAsInteger == 1) { return true; } throw new ConversionException("boolean is not one of [0, 1]"); } /** * Booleans attributes are converted to tags based on their names. */ @Override public Set<String> toTagSet(Object value, String name) { if (value == null) { String msg = "Illegal tag conversion from null on Attribute " + name + "."; throw new IllegalStateException(msg); } String tag = (Boolean) value ? name : "no" + name; return ImmutableSet.of(tag); } } private static class StringType extends Type<String> { @Override public String cast(Object value) { return (String) value; } @Override public String getDefaultValue() { return ""; } @Override public <T> void visitLabels(LabelVisitor<T> visitor, Object value, T context) { } @Override public String toString() { return "string"; } @Override public String convert(Object x, Object what, Object context) throws ConversionException { if (!(x instanceof String)) { throw new ConversionException(this, x, what); } return StringCanonicalizer.intern((String) x); } @Override public String concat(Iterable<String> elements) { return Joiner.on("").join(elements); } /** * A String is representable as a set containing its value. */ @Override public Set<String> toTagSet(Object value, String name) { if (value == null) { String msg = "Illegal tag conversion from null on Attribute " + name + "."; throw new IllegalStateException(msg); } return ImmutableSet.of((String) value); } } /** * A type to support dictionary attributes. */ public static class DictType<KeyT, ValueT> extends Type<Map<KeyT, ValueT>> { private final Type<KeyT> keyType; private final Type<ValueT> valueType; private final Map<KeyT, ValueT> empty = ImmutableMap.of(); private final LabelClass labelClass; @Override public <T> void visitLabels(LabelVisitor<T> visitor, Object value, T context) throws InterruptedException { for (Entry<KeyT, ValueT> entry : cast(value).entrySet()) { keyType.visitLabels(visitor, entry.getKey(), context); valueType.visitLabels(visitor, entry.getValue(), context); } } public static <KEY, VALUE> DictType<KEY, VALUE> create( Type<KEY> keyType, Type<VALUE> valueType) { LabelClass keyLabelClass = keyType.getLabelClass(); LabelClass valueLabelClass = valueType.getLabelClass(); Preconditions.checkArgument( keyLabelClass == LabelClass.NONE || valueLabelClass == LabelClass.NONE || keyLabelClass == valueLabelClass, "A DictType's keys and values must be the same class of label if both contain labels, " + "but the key type " + keyType + " contains " + keyLabelClass + " labels, while " + "the value type " + valueType + " contains " + valueLabelClass + " labels."); LabelClass labelClass = (keyLabelClass != LabelClass.NONE) ? keyLabelClass : valueLabelClass; return new DictType<>(keyType, valueType, labelClass); } protected DictType(Type<KeyT> keyType, Type<ValueT> valueType, LabelClass labelClass) { this.keyType = keyType; this.valueType = valueType; this.labelClass = labelClass; } public Type<KeyT> getKeyType() { return keyType; } public Type<ValueT> getValueType() { return valueType; } @Override public LabelClass getLabelClass() { return labelClass; } @SuppressWarnings("unchecked") @Override public Map<KeyT, ValueT> cast(Object value) { return (Map<KeyT, ValueT>) value; } @Override public String toString() { return "dict(" + keyType + ", " + valueType + ")"; } @Override public Map<KeyT, ValueT> convert(Object x, Object what, Object context) throws ConversionException { if (!(x instanceof Map<?, ?>)) { throw new ConversionException(this, x, what); } // Order the keys so the return value will be independent of insertion order. Map<KeyT, ValueT> result = new TreeMap<>(); Map<?, ?> o = (Map<?, ?>) x; for (Entry<?, ?> elem : o.entrySet()) { result.put( keyType.convert(elem.getKey(), "dict key element", context), valueType.convert(elem.getValue(), "dict value element", context)); } return ImmutableMap.copyOf(result); } @Override public Map<KeyT, ValueT> getDefaultValue() { return empty; } } /** A type for lists of a given element type */ public static class ListType<ElemT> extends Type<List<ElemT>> { private final Type<ElemT> elemType; private final List<ElemT> empty = ImmutableList.of(); public static <ELEM> ListType<ELEM> create(Type<ELEM> elemType) { return new ListType<>(elemType); } private ListType(Type<ElemT> elemType) { this.elemType = elemType; } @SuppressWarnings("unchecked") @Override public List<ElemT> cast(Object value) { return (List<ElemT>) value; } @Override public Type<ElemT> getListElementType() { return elemType; } @Override public LabelClass getLabelClass() { return elemType.getLabelClass(); } @Override public List<ElemT> getDefaultValue() { return empty; } @Override public <T> void visitLabels(LabelVisitor<T> visitor, Object value, T context) throws InterruptedException { for (ElemT elem : cast(value)) { elemType.visitLabels(visitor, elem, context); } } @Override public String toString() { return "list(" + elemType + ")"; } @Override public List<ElemT> convert(Object x, Object what, Object context) throws ConversionException { Iterable<?> iterable; try { iterable = EvalUtils.toIterableStrict(x, null); } catch (EvalException ex) { throw new ConversionException(this, x, what); } int index = 0; List<ElemT> result = new ArrayList<>(Iterables.size(iterable)); ListConversionContext conversionContext = new ListConversionContext(what); for (Object elem : iterable) { conversionContext.update(index); ElemT converted = elemType.convert(elem, conversionContext, context); if (converted != null) { result.add(converted); } else { // shouldn't happen but it does, rarely String message = "Converting a list with a null element: " + "element " + index + " of " + what + " in " + context; LoggingUtil.logToRemote(Level.WARNING, message, new ConversionException(message)); } ++index; } // We preserve GlobList-s so they can make it to attributes; // some external code relies on attributes preserving this information. // TODO(bazel-team): somehow make Skylark extensible enough that // GlobList support can be wholly moved out of Skylark into an extension. if (x instanceof GlobList<?>) { return new GlobList<>(((GlobList<?>) x).getCriteria(), result); } if (x instanceof MutableList) { GlobList<?> globList = ((MutableList) x).getGlobList(); if (globList != null) { return new GlobList<>(globList.getCriteria(), result); } } return result; } @Override public List<ElemT> concat(Iterable<List<ElemT>> elements) { ImmutableList.Builder<ElemT> builder = ImmutableList.builder(); for (List<ElemT> list : elements) { builder.addAll(list); } return builder.build(); } /** * A list is representable as a tag set as the contents of itself expressed * as Strings. So a {@code List<String>} is effectively converted to a {@code Set<String>}. */ @Override public Set<String> toTagSet(Object items, String name) { if (items == null) { String msg = "Illegal tag conversion from null on Attribute" + name + "."; throw new IllegalStateException(msg); } Set<String> tags = new LinkedHashSet<>(); @SuppressWarnings("unchecked") List<ElemT> itemsAsListofElem = (List<ElemT>) items; for (ElemT element : itemsAsListofElem) { tags.add(element.toString()); } return tags; } /** * Provides a {@link #toString()} description of the context of the value in a list being * converted. This is preferred over a raw string to avoid uselessly constructing strings which * are never used. This class is mutable (the index is updated). */ private static class ListConversionContext { private final Object what; private int index = 0; ListConversionContext(Object what) { this.what = what; } void update(int index) { this.index = index; } @Override public String toString() { return "element " + index + " of " + what; } } } /** Type for lists of arbitrary objects */ public static class ObjectListType extends ListType<Object> { private static final Type<Object> elemType = new ObjectType(); private ObjectListType() { super(elemType); } @Override @SuppressWarnings("unchecked") public List<Object> convert(Object x, Object what, Object context) throws ConversionException { if (x instanceof SkylarkList) { return ((SkylarkList) x).getImmutableList(); } else if (x instanceof List) { return (List<Object>) x; } else if (x instanceof Iterable) { // Do not remove <Object>: workaround for Java 7 type inference. return ImmutableList.<Object>copyOf((Iterable<?>) x); } else { throw new ConversionException(this, x, what); } } } /** * The type of a general list. */ public static final ListType<Object> LIST = new ListType<>(new ObjectType()); }