// 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.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.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.Collection; 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; /** *

Root of Type symbol hierarchy for values in the build language.

* *

Type symbols are primarily used for their convert method, * which is a kind of cast operator enabling conversion from untyped (Object) * references to values in the build language, to typed references.

* *

For example, this code type-converts a value x returned by * the evaluator, to a list of strings:

* *
 *  Object x = expr.eval(env);
 *  List<String> s = Type.STRING_LIST.convert(x);
 *  
*/ public abstract class Type { protected Type() {} /** * Converts untyped Object x resulting from the evaluation of an expression in the build language, * into a typed object of type T. * *

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 a string description of what x is for; should be included in * any exception thrown. Grammatically, must 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, String 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, String 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(); /** * Flatten the an instance of the type if the type is a composite one. * *

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 Collection flatten(Object value); /** * {@link #flatten} return value for types that don't contain labels. */ protected static final Collection NOT_COMPOSITE_TYPE = ImmutableList.of(); /** * Implementation of concatenation for this type (e.g. "val1 + val2"). Returns null to * indicate concatenation isn't supported. */ public T concat(@SuppressWarnings("unused") Iterable 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 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 = new IntegerType(); /** * The type of a string. */ public static final Type STRING = new StringType(); /** * The type of a boolean. */ public static final Type 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_LIST = ListType.create(STRING); /** * The type of a list of {@linkplain #INTEGER strings}. */ public static final ListType INTEGER_LIST = ListType.create(INTEGER); /** * The type of a dictionary of {@linkplain #STRING strings}. */ public static final DictType STRING_DICT = DictType.create(STRING, STRING); /** * The type of a dictionary of {@linkplain #STRING_LIST label lists}. */ public static final DictType> STRING_LIST_DICT = DictType.create(STRING, STRING_LIST); /** * The type of a dictionary of {@linkplain #STRING strings}, where each entry * maps to a single string value. */ public static final DictType STRING_DICT_UNARY = DictType.create(STRING, STRING); /** * 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, String 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, String what) { super(null, message(type, value, what)); } public ConversionException(String message) { super(null, message); } } /******************************************************************** * * * Subclasses * * * ********************************************************************/ private static class ObjectType extends Type { @Override public Object cast(Object value) { return value; } @Override public String getDefaultValue() { throw new UnsupportedOperationException( "ObjectType has no default value"); } @Override public Collection flatten(Object value) { return NOT_COMPOSITE_TYPE; } @Override public String toString() { return "object"; } @Override public Object convert(Object x, String what, Object context) { return x; } } private static class IntegerType extends Type { @Override public Integer cast(Object value) { return (Integer) value; } @Override public Integer getDefaultValue() { return 0; } @Override public Collection flatten(Object value) { return NOT_COMPOSITE_TYPE; } @Override public String toString() { return "int"; } @Override public Integer convert(Object x, String what, Object context) throws ConversionException { if (!(x instanceof Integer)) { throw new ConversionException(this, x, what); } return (Integer) x; } @Override public Integer concat(Iterable elements) { int ans = 0; for (Integer elem : elements) { ans += elem; } return Integer.valueOf(ans); } } private static class BooleanType extends Type { @Override public Boolean cast(Object value) { return (Boolean) value; } @Override public Boolean getDefaultValue() { return false; } @Override public Collection flatten(Object value) { return NOT_COMPOSITE_TYPE; } @Override public String toString() { return "boolean"; } // Conversion to boolean must also tolerate integers of 0 and 1 only. @Override public Boolean convert(Object x, String 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 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 { @Override public String cast(Object value) { return (String) value; } @Override public String getDefaultValue() { return ""; } @Override public Collection flatten(Object value) { return NOT_COMPOSITE_TYPE; } @Override public String toString() { return "string"; } @Override public String convert(Object x, String 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 elements) { return Joiner.on("").join(elements); } /** * A String is representable as a set containing its value. */ @Override public Set 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 extends Type> { private final Type keyType; private final Type valueType; private final Map empty = ImmutableMap.of(); public static DictType create( Type keyType, Type valueType) { return new DictType<>(keyType, valueType); } private DictType(Type keyType, Type valueType) { this.keyType = keyType; this.valueType = valueType; } public Type getKeyType() { return keyType; } public Type getValueType() { return valueType; } @SuppressWarnings("unchecked") @Override public Map cast(Object value) { return (Map) value; } @Override public String toString() { return "dict(" + keyType + ", " + valueType + ")"; } @Override public Map convert(Object x, String what, Object context) throws ConversionException { if (!(x instanceof Map)) { throw new ConversionException(String.format( "Expected a map for dictionary but got a %s", x.getClass().getName())); } // Order the keys so the return value will be independent of insertion order. Map 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 getDefaultValue() { return empty; } @Override public Collection flatten(Object value) { ImmutableList.Builder result = ImmutableList.builder(); for (Map.Entry entry : cast(value).entrySet()) { result.addAll(keyType.flatten(entry.getKey())); result.addAll(valueType.flatten(entry.getValue())); } return result.build(); } } /** A type for lists of a given element type */ public static class ListType extends Type> { private final Type elemType; private final List empty = ImmutableList.of(); public static ListType create(Type elemType) { return new ListType<>(elemType); } private ListType(Type elemType) { this.elemType = elemType; } @SuppressWarnings("unchecked") @Override public List cast(Object value) { return (List) value; } @Override public Type getListElementType() { return elemType; } @Override public List getDefaultValue() { return empty; } @Override public Collection flatten(Object value) { ImmutableList.Builder labels = ImmutableList.builder(); for (ElemT entry : cast(value)) { labels.addAll(elemType.flatten(entry)); } return labels.build(); } @Override public String toString() { return "list(" + elemType + ")"; } @Override public List convert(Object x, String what, Object context) throws ConversionException { if (!(x instanceof Iterable)) { throw new ConversionException(this, x, what); } int index = 0; Iterable iterable = (Iterable) x; List result = new ArrayList<>(Iterables.size(iterable)); for (Object elem : iterable) { ElemT converted = elemType.convert(elem, "element " + index + " of " + what, 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 concat(Iterable> elements) { ImmutableList.Builder builder = ImmutableList.builder(); for (List 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} is effectively converted to a {@code Set}. */ @Override public Set toTagSet(Object items, String name) { if (items == null) { String msg = "Illegal tag conversion from null on Attribute" + name + "."; throw new IllegalStateException(msg); } Set tags = new LinkedHashSet<>(); @SuppressWarnings("unchecked") List itemsAsListofElem = (List) items; for (ElemT element : itemsAsListofElem) { tags.add(element.toString()); } return tags; } } /** Type for lists of arbitrary objects */ public static class ObjectListType extends ListType { private static final Type elemType = new ObjectType(); private ObjectListType() { super(elemType); } @Override @SuppressWarnings("unchecked") public List convert(Object x, String what, Object context) throws ConversionException { if (x instanceof SkylarkList) { return ((SkylarkList) x).getImmutableList(); } else if (x instanceof List) { return (List) x; } else if (x instanceof Iterable) { // Do not remove : workaround for Java 7 type inference. return ImmutableList.copyOf((Iterable) x); } else { throw new ConversionException(this, x, what); } } } /** * The type of a general list. */ public static final ListType LIST = new ListType<>(new ObjectType()); }