// 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.common.options; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import javax.annotation.concurrent.Immutable; /** * An immutable selection of options data corresponding to a set of options classes. The data is * collected using reflection, which can be expensive. Therefore this class can be used internally * to cache the results. * * <p>The data is isolated in the sense that it has not yet been processed to add inter-option- * dependent information -- namely, the results of evaluating expansion functions. The {@link * OptionsData} subclass stores this added information. The reason for the split is so that we can * avoid exposing to expansion functions the effects of evaluating other expansion functions, to * ensure that the order in which they run is not significant. */ // TODO(brandjon): This class is technically not necessarily immutable due to optionsDefault // accepting Object values, and the List in allOptionsField should be ImmutableList. Either fix // this or remove @Immutable. @Immutable class IsolatedOptionsData extends OpaqueOptionsData { /** * These are the options-declaring classes which are annotated with {@link Option} annotations. */ private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses; /** Maps option name to Option-annotated Field. */ private final ImmutableMap<String, Field> nameToField; /** Maps option abbreviation to Option-annotated Field. */ private final ImmutableMap<Character, Field> abbrevToField; /** For each options class, contains a list of all Option-annotated fields in that class. */ private final ImmutableMap<Class<? extends OptionsBase>, List<Field>> allOptionsFields; /** Mapping from each Option-annotated field to the default value for that field. */ // Immutable like the others, but uses Collections.unmodifiableMap because of null values. private final Map<Field, Object> optionDefaults; /** * Mapping from each Option-annotated field to the proper converter. * * @see #findConverter */ private final ImmutableMap<Field, Converter<?>> converters; /** * Mapping from each Option-annotated field to a boolean for whether that field allows multiple * values. */ private final ImmutableMap<Field, Boolean> allowMultiple; private IsolatedOptionsData( Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, Map<String, Field> nameToField, Map<Character, Field> abbrevToField, Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields, Map<Field, Object> optionDefaults, Map<Field, Converter<?>> converters, Map<Field, Boolean> allowMultiple) { this.optionsClasses = ImmutableMap.copyOf(optionsClasses); this.nameToField = ImmutableMap.copyOf(nameToField); this.abbrevToField = ImmutableMap.copyOf(abbrevToField); this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields); // Can't use an ImmutableMap here because of null values. this.optionDefaults = Collections.unmodifiableMap(optionDefaults); this.converters = ImmutableMap.copyOf(converters); this.allowMultiple = ImmutableMap.copyOf(allowMultiple); } protected IsolatedOptionsData(IsolatedOptionsData other) { this( other.optionsClasses, other.nameToField, other.abbrevToField, other.allOptionsFields, other.optionDefaults, other.converters, other.allowMultiple); } public Collection<Class<? extends OptionsBase>> getOptionsClasses() { return optionsClasses.keySet(); } @SuppressWarnings("unchecked") // The construction ensures that the case is always valid. public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) { return (Constructor<T>) optionsClasses.get(clazz); } public Field getFieldFromName(String name) { return nameToField.get(name); } public Iterable<Map.Entry<String, Field>> getAllNamedFields() { return nameToField.entrySet(); } public Field getFieldForAbbrev(char abbrev) { return abbrevToField.get(abbrev); } public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) { return allOptionsFields.get(optionsClass); } public Object getDefaultValue(Field field) { return optionDefaults.get(field); } public Converter<?> getConverter(Field field) { return converters.get(field); } public boolean getAllowMultiple(Field field) { return allowMultiple.get(field); } /** * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option * that does use it, asserts that the type is a {@code List<T>} and returns its element type * {@code T}. */ private static Type getFieldSingularType(Field field, Option annotation) { Type fieldType = field.getGenericType(); if (annotation.allowMultiple()) { // If the type isn't a List<T>, this is an error in the option's declaration. if (!(fieldType instanceof ParameterizedType)) { throw new AssertionError("Type of multiple occurrence option must be a List<...>"); } ParameterizedType pfieldType = (ParameterizedType) fieldType; if (pfieldType.getRawType() != List.class) { throw new AssertionError("Type of multiple occurrence option must be a List<...>"); } fieldType = pfieldType.getActualTypeArguments()[0]; } return fieldType; } /** * Returns whether a field should be considered as boolean. * * <p>Can be used for usage help and controlling whether the "no" prefix is allowed. */ static boolean isBooleanField(Field field) { return field.getType().equals(boolean.class) || field.getType().equals(TriState.class) || findConverter(field) instanceof BoolOrEnumConverter; } /** Returns whether a field has Void type. */ static boolean isVoidField(Field field) { return field.getType().equals(Void.class); } /** * Returns whether the arg is an expansion option defined by an expansion function (and not a * constant expansion value). */ static boolean usesExpansionFunction(Option annotation) { return annotation.expansionFunction() != ExpansionFunction.class; } /** * Given an {@code @Option}-annotated field, retrieves the {@link Converter} that will be used, * taking into account the default converters if an explicit one is not specified. */ static Converter<?> findConverter(Field optionField) { Option annotation = optionField.getAnnotation(Option.class); if (annotation.converter() == Converter.class) { // No converter provided, use the default one. Type type = getFieldSingularType(optionField, annotation); Converter<?> converter = Converters.DEFAULT_CONVERTERS.get(type); if (converter == null) { throw new AssertionError( "No converter found for " + type + "; possible fix: add " + "converter=... to @Option annotation for " + optionField.getName()); } return converter; } try { // Instantiate the given Converter class. Class<?> converter = annotation.converter(); Constructor<?> constructor = converter.getConstructor(); return (Converter<?>) constructor.newInstance(); } catch (Exception e) { // This indicates an error in the Converter, and should be discovered the first time it is // used. throw new AssertionError(e); } } private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) { List<Field> allFields = Lists.newArrayList(); for (Field field : optionsClass.getFields()) { if (field.isAnnotationPresent(Option.class)) { allFields.add(field); } } if (allFields.isEmpty()) { throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields"); } return ImmutableList.copyOf(allFields); } private static Object retrieveDefaultFromAnnotation(Field optionField) { Converter<?> converter = findConverter(optionField); String defaultValueAsString = OptionsParserImpl.getDefaultOptionString(optionField); // Special case for "null" if (OptionsParserImpl.isSpecialNullDefault(defaultValueAsString, optionField)) { return null; } boolean allowsMultiple = optionField.getAnnotation(Option.class).allowMultiple(); // If the option allows multiple values then we intentionally return the empty list as // the default value of this option since it is not always the case that an option // that allows multiple values will have a converter that returns a list value. if (allowsMultiple) { return Collections.emptyList(); } // Otherwise try to convert the default value using the converter Object convertedValue; try { convertedValue = converter.convert(defaultValueAsString); } catch (OptionsParsingException e) { throw new IllegalStateException("OptionsParsingException while " + "retrieving default for " + optionField.getName() + ": " + e.getMessage()); } return convertedValue; } /** * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking * on each option in isolation. */ static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) { Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = Maps.newHashMap(); Map<Class<? extends OptionsBase>, List<Field>> allOptionsFieldsBuilder = Maps.newHashMap(); Map<String, Field> nameToFieldBuilder = Maps.newHashMap(); Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap(); Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap(); Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap(); Map<Field, Boolean> allowMultipleBuilder = Maps.newHashMap(); // Read all Option annotations: for (Class<? extends OptionsBase> parsedOptionsClass : classes) { try { Constructor<? extends OptionsBase> constructor = parsedOptionsClass.getConstructor(); constructorBuilder.put(parsedOptionsClass, constructor); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(parsedOptionsClass + " lacks an accessible default constructor"); } List<Field> fields = getAllAnnotatedFields(parsedOptionsClass); allOptionsFieldsBuilder.put(parsedOptionsClass, fields); for (Field field : fields) { Option annotation = field.getAnnotation(Option.class); if (annotation.name() == null) { throw new AssertionError("Option cannot have a null name"); } Type fieldType = getFieldSingularType(field, annotation); // Get the converter return type. @SuppressWarnings("rawtypes") Class<? extends Converter> converter = annotation.converter(); if (converter == Converter.class) { Converter<?> actualConverter = Converters.DEFAULT_CONVERTERS.get(fieldType); if (actualConverter == null) { throw new AssertionError("Cannot find converter for field of type " + field.getType() + " named " + field.getName() + " in class " + field.getDeclaringClass().getName()); } converter = actualConverter.getClass(); } if (Modifier.isAbstract(converter.getModifiers())) { throw new AssertionError("The converter type " + converter + " must be a concrete type"); } Type converterResultType; try { Method convertMethod = converter.getMethod("convert", String.class); converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod); } catch (NoSuchMethodException e) { throw new AssertionError("A known converter object doesn't implement the convert" + " method"); } if (annotation.allowMultiple()) { if (GenericTypeHelper.getRawType(converterResultType) == List.class) { Type elementType = ((ParameterizedType) converterResultType).getActualTypeArguments()[0]; if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) { throw new AssertionError("If the converter return type of a multiple occurance " + "option is a list, then the type of list elements (" + fieldType + ") must be " + "assignable from the converter list element type (" + elementType + ")"); } } else { if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { throw new AssertionError("Type of list elements (" + fieldType + ") for multiple occurrence option must be assignable from the converter " + "return type (" + converterResultType + ")"); } } } else { if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { throw new AssertionError("Type of field (" + fieldType + ") must be assignable from the converter " + "return type (" + converterResultType + ")"); } } if (nameToFieldBuilder.put(annotation.name(), field) != null) { throw new DuplicateOptionDeclarationException( "Duplicate option name: --" + annotation.name()); } if (!annotation.oldName().isEmpty()) { if (nameToFieldBuilder.put(annotation.oldName(), field) != null) { throw new DuplicateOptionDeclarationException( "Old option name duplicates option name: --" + annotation.oldName()); } } if (annotation.abbrev() != '\0') { if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) { throw new DuplicateOptionDeclarationException( "Duplicate option abbrev: -" + annotation.abbrev()); } } optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field)); convertersBuilder.put(field, findConverter(field)); allowMultipleBuilder.put(field, annotation.allowMultiple()); } } return new IsolatedOptionsData( constructorBuilder, nameToFieldBuilder, abbrevToFieldBuilder, allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder, allowMultipleBuilder); } }