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

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. * *

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. * *

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 Map, ImmutableList> allOptionsFields = new HashMap<>(); /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */ public static synchronized ImmutableList getAllOptionDefinitionsForClass( Class 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, 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 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 oldNameToField; /** Mapping from option abbreviation to {@code OptionDefinition} (unordered). */ private final ImmutableMap abbrevToField; /** * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes} * annotation (unordered). */ private final ImmutableMap, Boolean> usesOnlyCoreTypes; private IsolatedOptionsData( Map, Constructor> optionsClasses, Map nameToField, Map oldNameToField, Map abbrevToField, Map, 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> getOptionsClasses() { return optionsClasses.keySet(); } @SuppressWarnings("unchecked") // The construction ensures that the case is always valid. public Constructor getConstructor(Class clazz) { return (Constructor) 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> getAllOptionDefinitions() { return nameToField.entrySet(); } public OptionDefinition getFieldForAbbrev(char abbrev) { return abbrevToField.get(abbrev); } public boolean getUsesOnlyCoreTypes(Class 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 void checkForCollisions( Map 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 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 nameToFieldMap, Map oldNameToFieldMap, Map 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> classes) { // Mind which fields have to preserve order. Map, Constructor> constructorBuilder = new LinkedHashMap<>(); Map nameToFieldBuilder = new LinkedHashMap<>(); Map oldNameToFieldBuilder = new LinkedHashMap<>(); Map abbrevToFieldBuilder = new HashMap<>(); // Maps the negated boolean flag aliases to the original option name. Map booleanAliasMap = new HashMap<>(); Map, 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 parsedOptionsClass : classes) { try { Constructor constructor = parsedOptionsClass.getConstructor(); constructorBuilder.put(parsedOptionsClass, constructor); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(parsedOptionsClass + " lacks an accessible default constructor"); } ImmutableList 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); } }