| // 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.devtools.common.options.OptionDefinition.NotAnOptionException; | |
| import com.google.devtools.common.options.OptionsParser.ConstructionException; | |
| import java.lang.reflect.Constructor; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.HashMap; | |
| import java.util.LinkedHashMap; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ConcurrentMap; | |
| import javax.annotation.concurrent.Immutable; | |
| /** | |
| * A selection of options data corresponding to a set of {@link OptionsBase} subclasses (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. | |
| * | |
| * <p>This class is immutable so long as the converters and default values associated with the | |
| * options are immutable. | |
| */ | |
| @Immutable | |
| public class IsolatedOptionsData extends OpaqueOptionsData { | |
| /** | |
| * Cache for the options in an OptionsBase. | |
| * | |
| * <p>Mapping from options class to a list of all {@code OptionFields} in that class. The map | |
| * entries are unordered, but the fields in the lists are ordered alphabetically. This caches the | |
| * work of reflection done for the same {@code optionsBase} across multiple {@link OptionsData} | |
| * instances, and must be used through the thread safe {@link | |
| * #getAllOptionDefinitionsForClass(Class)} | |
| */ | |
| private static final ConcurrentMap<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>> | |
| allOptionsFields = new ConcurrentHashMap<>(); | |
| /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */ | |
| public static ImmutableList<OptionDefinition> getAllOptionDefinitionsForClass( | |
| Class<? extends OptionsBase> optionsClass) { | |
| return allOptionsFields.computeIfAbsent( | |
| optionsClass, | |
| optionsBaseClass -> | |
| Arrays.stream(optionsBaseClass.getFields()) | |
| .map( | |
| field -> { | |
| try { | |
| return OptionDefinition.extractOptionDefinition(field); | |
| } catch (NotAnOptionException e) { | |
| // Ignore non-@Option annotated fields. Requiring all fields in the | |
| // OptionsBase to be @Option-annotated requires a depot cleanup. | |
| return null; | |
| } | |
| }) | |
| .filter(Objects::nonNull) | |
| .sorted(OptionDefinition.BY_OPTION_NAME) | |
| .collect(ImmutableList.toImmutableList())); | |
| } | |
| /** | |
| * Mapping from each options class to its no-arg constructor. Entries appear in the same order | |
| * that they were passed to {@link #from(Collection)}. | |
| */ | |
| private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses; | |
| /** | |
| * Mapping from option name to {@code OptionDefinition}. Entries appear ordered first by their | |
| * options class (the order in which they were passed to {@link #from(Collection)}, and then in | |
| * alphabetic order within each options class. | |
| */ | |
| private final ImmutableMap<String, OptionDefinition> nameToField; | |
| /** | |
| * For options that have an "OldName", this is a mapping from old name to its corresponding {@code | |
| * OptionDefinition}. Entries appear ordered first by their options class (the order in which they | |
| * were passed to {@link #from(Collection)}, and then in alphabetic order within each options | |
| * class. | |
| */ | |
| private final ImmutableMap<String, OptionDefinition> oldNameToField; | |
| /** Mapping from option abbreviation to {@code OptionDefinition} (unordered). */ | |
| private final ImmutableMap<Character, OptionDefinition> abbrevToField; | |
| /** | |
| * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes} | |
| * annotation (unordered). | |
| */ | |
| private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes; | |
| private IsolatedOptionsData( | |
| Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, | |
| Map<String, OptionDefinition> nameToField, | |
| Map<String, OptionDefinition> oldNameToField, | |
| Map<Character, OptionDefinition> abbrevToField, | |
| Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) { | |
| this.optionsClasses = ImmutableMap.copyOf(optionsClasses); | |
| this.nameToField = ImmutableMap.copyOf(nameToField); | |
| this.oldNameToField = ImmutableMap.copyOf(oldNameToField); | |
| this.abbrevToField = ImmutableMap.copyOf(abbrevToField); | |
| this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes); | |
| } | |
| protected IsolatedOptionsData(IsolatedOptionsData other) { | |
| this( | |
| other.optionsClasses, | |
| other.nameToField, | |
| other.oldNameToField, | |
| other.abbrevToField, | |
| other.usesOnlyCoreTypes); | |
| } | |
| /** | |
| * Returns all options classes indexed by this options data object, in the order they were passed | |
| * to {@link #from(Collection)}. | |
| */ | |
| 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); | |
| } | |
| /** | |
| * Returns the option in this parser by the provided name, or {@code null} if none is found. This | |
| * will match both the canonical name of an option, and any old name listed that we still accept. | |
| */ | |
| public OptionDefinition getOptionDefinitionFromName(String name) { | |
| return nameToField.getOrDefault(name, oldNameToField.get(name)); | |
| } | |
| /** | |
| * Returns all {@link OptionDefinition} objects loaded, mapped by their canonical names. Entries | |
| * appear ordered first by their options class (the order in which they were passed to {@link | |
| * #from(Collection)}, and then in alphabetic order within each options class. | |
| */ | |
| public Iterable<Map.Entry<String, OptionDefinition>> getAllOptionDefinitions() { | |
| return nameToField.entrySet(); | |
| } | |
| public OptionDefinition getFieldForAbbrev(char abbrev) { | |
| return abbrevToField.get(abbrev); | |
| } | |
| public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) { | |
| return usesOnlyCoreTypes.get(optionsClass); | |
| } | |
| /** | |
| * Generic method to check for collisions between the names we give options. Useful for checking | |
| * both single-character abbreviations and full names. | |
| */ | |
| private static <A> void checkForCollisions( | |
| Map<A, OptionDefinition> aFieldMap, A optionName, String description) | |
| throws DuplicateOptionDeclarationException { | |
| if (aFieldMap.containsKey(optionName)) { | |
| throw new DuplicateOptionDeclarationException( | |
| "Duplicate option name, due to " + description + ": --" + optionName); | |
| } | |
| } | |
| /** | |
| * All options, even non-boolean ones, should check that they do not conflict with previously | |
| * loaded boolean options. | |
| */ | |
| private static void checkForBooleanAliasCollisions( | |
| Map<String, String> booleanAliasMap, String optionName, String description) | |
| throws DuplicateOptionDeclarationException { | |
| if (booleanAliasMap.containsKey(optionName)) { | |
| throw new DuplicateOptionDeclarationException( | |
| "Duplicate option name, due to " | |
| + description | |
| + " --" | |
| + optionName | |
| + ", it conflicts with a negating alias for boolean flag --" | |
| + booleanAliasMap.get(optionName)); | |
| } | |
| } | |
| /** | |
| * For an {@code option} of boolean type, this checks that the boolean alias does not conflict | |
| * with other names, and adds the boolean alias to a list so that future flags can find if they | |
| * conflict with a boolean alias.. | |
| */ | |
| private static void checkAndUpdateBooleanAliases( | |
| Map<String, OptionDefinition> nameToFieldMap, | |
| Map<String, OptionDefinition> oldNameToFieldMap, | |
| Map<String, String> booleanAliasMap, | |
| String optionName) | |
| throws DuplicateOptionDeclarationException { | |
| // Check that the negating alias does not conflict with existing flags. | |
| checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias"); | |
| checkForCollisions(oldNameToFieldMap, "no" + optionName, "boolean option alias"); | |
| // Record that the boolean option takes up additional namespace for its negating alias. | |
| booleanAliasMap.put("no" + optionName, optionName); | |
| } | |
| /** | |
| * 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) { | |
| // Mind which fields have to preserve order. | |
| Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>(); | |
| Map<String, OptionDefinition> nameToFieldBuilder = new LinkedHashMap<>(); | |
| Map<String, OptionDefinition> oldNameToFieldBuilder = new LinkedHashMap<>(); | |
| Map<Character, OptionDefinition> abbrevToFieldBuilder = new HashMap<>(); | |
| // Maps the negated boolean flag aliases to the original option name. | |
| Map<String, String> booleanAliasMap = new HashMap<>(); | |
| Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>(); | |
| // Combine the option definitions for these options classes, and check that they do not | |
| // conflict. The options are individually checked for correctness at compile time in the | |
| // OptionProcessor. | |
| 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"); | |
| } | |
| ImmutableList<OptionDefinition> optionDefinitions = | |
| getAllOptionDefinitionsForClass(parsedOptionsClass); | |
| for (OptionDefinition optionDefinition : optionDefinitions) { | |
| try { | |
| String optionName = optionDefinition.getOptionName(); | |
| checkForCollisions(nameToFieldBuilder, optionName, "option name collision"); | |
| checkForCollisions( | |
| oldNameToFieldBuilder, | |
| optionName, | |
| "option name collision with another option's old name"); | |
| checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option"); | |
| if (optionDefinition.usesBooleanValueSyntax()) { | |
| checkAndUpdateBooleanAliases( | |
| nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, optionName); | |
| } | |
| nameToFieldBuilder.put(optionName, optionDefinition); | |
| if (!optionDefinition.getOldOptionName().isEmpty()) { | |
| String oldName = optionDefinition.getOldOptionName(); | |
| checkForCollisions( | |
| nameToFieldBuilder, | |
| oldName, | |
| "old option name collision with another option's canonical name"); | |
| checkForCollisions( | |
| oldNameToFieldBuilder, | |
| oldName, | |
| "old option name collision with another old option name"); | |
| checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name"); | |
| // If boolean, repeat the alias dance for the old name. | |
| if (optionDefinition.usesBooleanValueSyntax()) { | |
| checkAndUpdateBooleanAliases( | |
| nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, oldName); | |
| } | |
| // Now that we've checked for conflicts, confidently store the old name. | |
| oldNameToFieldBuilder.put(oldName, optionDefinition); | |
| } | |
| if (optionDefinition.getAbbreviation() != '\0') { | |
| checkForCollisions( | |
| abbrevToFieldBuilder, optionDefinition.getAbbreviation(), "option abbreviation"); | |
| abbrevToFieldBuilder.put(optionDefinition.getAbbreviation(), optionDefinition); | |
| } | |
| } catch (DuplicateOptionDeclarationException e) { | |
| throw new ConstructionException(e); | |
| } | |
| } | |
| boolean usesOnlyCoreTypes = parsedOptionsClass.isAnnotationPresent(UsesOnlyCoreTypes.class); | |
| if (usesOnlyCoreTypes) { | |
| // Validate that @UsesOnlyCoreTypes was used correctly. | |
| for (OptionDefinition optionDefinition : optionDefinitions) { | |
| // The classes in coreTypes are all final. But even if they weren't, we only want to check | |
| // for exact matches; subclasses would not be considered core types. | |
| if (!UsesOnlyCoreTypes.CORE_TYPES.contains(optionDefinition.getType())) { | |
| throw new ConstructionException( | |
| "Options class '" | |
| + parsedOptionsClass.getName() | |
| + "' is marked as " | |
| + "@UsesOnlyCoreTypes, but field '" | |
| + optionDefinition.getField().getName() | |
| + "' has type '" | |
| + optionDefinition.getType().getName() | |
| + "'"); | |
| } | |
| } | |
| } | |
| usesOnlyCoreTypesBuilder.put(parsedOptionsClass, usesOnlyCoreTypes); | |
| } | |
| return new IsolatedOptionsData( | |
| constructorBuilder, | |
| nameToFieldBuilder, | |
| oldNameToFieldBuilder, | |
| abbrevToFieldBuilder, | |
| usesOnlyCoreTypesBuilder); | |
| } | |
| } |