328 lines
14 KiB
Java
328 lines
14 KiB
Java
// 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);
|
|
}
|
|
|
|
}
|