// 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.
*
* <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 Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>>
allOptionsFields = new HashMap<>();
/** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */
public static synchronized 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);
}
}