1 // Copyright 2014 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.devtools.common.options; 16 17 import com.google.common.collect.ImmutableList; 18 import com.google.common.collect.ImmutableMap; 19 import com.google.devtools.common.options.OptionDefinition.NotAnOptionException; 20 import com.google.devtools.common.options.OptionsParser.ConstructionException; 21 import java.lang.reflect.Constructor; 22 import java.util.Arrays; 23 import java.util.Collection; 24 import java.util.HashMap; 25 import java.util.LinkedHashMap; 26 import java.util.Map; 27 import java.util.Objects; 28 import javax.annotation.concurrent.Immutable; 29 30 /** 31 * A selection of options data corresponding to a set of {@link OptionsBase} subclasses (options 32 * classes). The data is collected using reflection, which can be expensive. Therefore this class 33 * can be used internally to cache the results. 34 * 35 * <p>The data is isolated in the sense that it has not yet been processed to add 36 * inter-option-dependent information -- namely, the results of evaluating expansion functions. The 37 * {@link OptionsData} subclass stores this added information. The reason for the split is so that 38 * we can avoid exposing to expansion functions the effects of evaluating other expansion functions, 39 * to ensure that the order in which they run is not significant. 40 * 41 * <p>This class is immutable so long as the converters and default values associated with the 42 * options are immutable. 43 */ 44 @Immutable 45 public class IsolatedOptionsData extends OpaqueOptionsData { 46 47 /** 48 * Cache for the options in an OptionsBase. 49 * 50 * <p>Mapping from options class to a list of all {@code OptionFields} in that class. The map 51 * entries are unordered, but the fields in the lists are ordered alphabetically. This caches the 52 * work of reflection done for the same {@code optionsBase} across multiple {@link OptionsData} 53 * instances, and must be used through the thread safe {@link 54 * #getAllOptionDefinitionsForClass(Class)} 55 */ 56 private static final Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>> 57 allOptionsFields = new HashMap<>(); 58 59 /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */ getAllOptionDefinitionsForClass( Class<? extends OptionsBase> optionsClass)60 public static synchronized ImmutableList<OptionDefinition> getAllOptionDefinitionsForClass( 61 Class<? extends OptionsBase> optionsClass) { 62 return allOptionsFields.computeIfAbsent( 63 optionsClass, 64 optionsBaseClass -> 65 Arrays.stream(optionsBaseClass.getFields()) 66 .map( 67 field -> { 68 try { 69 return OptionDefinition.extractOptionDefinition(field); 70 } catch (NotAnOptionException e) { 71 // Ignore non-@Option annotated fields. Requiring all fields in the 72 // OptionsBase to be @Option-annotated requires a depot cleanup. 73 return null; 74 } 75 }) 76 .filter(Objects::nonNull) 77 .sorted(OptionDefinition.BY_OPTION_NAME) 78 .collect(ImmutableList.toImmutableList())); 79 } 80 81 /** 82 * Mapping from each options class to its no-arg constructor. Entries appear in the same order 83 * that they were passed to {@link #from(Collection)}. 84 */ 85 private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses; 86 87 /** 88 * Mapping from option name to {@code OptionDefinition}. Entries appear ordered first by their 89 * options class (the order in which they were passed to {@link #from(Collection)}, and then in 90 * alphabetic order within each options class. 91 */ 92 private final ImmutableMap<String, OptionDefinition> nameToField; 93 94 /** 95 * For options that have an "OldName", this is a mapping from old name to its corresponding {@code 96 * OptionDefinition}. Entries appear ordered first by their options class (the order in which they 97 * were passed to {@link #from(Collection)}, and then in alphabetic order within each options 98 * class. 99 */ 100 private final ImmutableMap<String, OptionDefinition> oldNameToField; 101 102 /** Mapping from option abbreviation to {@code OptionDefinition} (unordered). */ 103 private final ImmutableMap<Character, OptionDefinition> abbrevToField; 104 105 106 /** 107 * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes} 108 * annotation (unordered). 109 */ 110 private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes; 111 112 private IsolatedOptionsData( 113 Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, 114 Map<String, OptionDefinition> nameToField, 115 Map<String, OptionDefinition> oldNameToField, 116 Map<Character, OptionDefinition> abbrevToField, 117 Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) { 118 this.optionsClasses = ImmutableMap.copyOf(optionsClasses); 119 this.nameToField = ImmutableMap.copyOf(nameToField); 120 this.oldNameToField = ImmutableMap.copyOf(oldNameToField); 121 this.abbrevToField = ImmutableMap.copyOf(abbrevToField); 122 this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes); 123 } 124 125 protected IsolatedOptionsData(IsolatedOptionsData other) { 126 this( 127 other.optionsClasses, 128 other.nameToField, 129 other.oldNameToField, 130 other.abbrevToField, 131 other.usesOnlyCoreTypes); 132 } 133 134 /** 135 * Returns all options classes indexed by this options data object, in the order they were passed 136 * to {@link #from(Collection)}. 137 */ 138 public Collection<Class<? extends OptionsBase>> getOptionsClasses() { 139 return optionsClasses.keySet(); 140 } 141 142 @SuppressWarnings("unchecked") // The construction ensures that the case is always valid. 143 public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) { 144 return (Constructor<T>) optionsClasses.get(clazz); 145 } 146 147 /** 148 * Returns the option in this parser by the provided name, or {@code null} if none is found. This 149 * will match both the canonical name of an option, and any old name listed that we still accept. 150 */ 151 public OptionDefinition getOptionDefinitionFromName(String name) { 152 return nameToField.getOrDefault(name, oldNameToField.get(name)); 153 } 154 155 /** 156 * Returns all {@link OptionDefinition} objects loaded, mapped by their canonical names. Entries 157 * appear ordered first by their options class (the order in which they were passed to {@link 158 * #from(Collection)}, and then in alphabetic order within each options class. 159 */ 160 public Iterable<Map.Entry<String, OptionDefinition>> getAllOptionDefinitions() { 161 return nameToField.entrySet(); 162 } 163 164 public OptionDefinition getFieldForAbbrev(char abbrev) { 165 return abbrevToField.get(abbrev); 166 } 167 168 public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) { 169 return usesOnlyCoreTypes.get(optionsClass); 170 } 171 172 /** 173 * Generic method to check for collisions between the names we give options. Useful for checking 174 * both single-character abbreviations and full names. 175 */ 176 private static <A> void checkForCollisions( 177 Map<A, OptionDefinition> aFieldMap, A optionName, String description) 178 throws DuplicateOptionDeclarationException { 179 if (aFieldMap.containsKey(optionName)) { 180 throw new DuplicateOptionDeclarationException( 181 "Duplicate option name, due to " + description + ": --" + optionName); 182 } 183 } 184 185 /** 186 * All options, even non-boolean ones, should check that they do not conflict with previously 187 * loaded boolean options. 188 */ 189 private static void checkForBooleanAliasCollisions( 190 Map<String, String> booleanAliasMap, String optionName, String description) 191 throws DuplicateOptionDeclarationException { 192 if (booleanAliasMap.containsKey(optionName)) { 193 throw new DuplicateOptionDeclarationException( 194 "Duplicate option name, due to " 195 + description 196 + " --" 197 + optionName 198 + ", it conflicts with a negating alias for boolean flag --" 199 + booleanAliasMap.get(optionName)); 200 } 201 } 202 203 /** 204 * For an {@code option} of boolean type, this checks that the boolean alias does not conflict 205 * with other names, and adds the boolean alias to a list so that future flags can find if they 206 * conflict with a boolean alias.. 207 */ 208 private static void checkAndUpdateBooleanAliases( 209 Map<String, OptionDefinition> nameToFieldMap, 210 Map<String, OptionDefinition> oldNameToFieldMap, 211 Map<String, String> booleanAliasMap, 212 String optionName) 213 throws DuplicateOptionDeclarationException { 214 // Check that the negating alias does not conflict with existing flags. 215 checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias"); 216 checkForCollisions(oldNameToFieldMap, "no" + optionName, "boolean option alias"); 217 218 // Record that the boolean option takes up additional namespace for its negating alias. 219 booleanAliasMap.put("no" + optionName, optionName); 220 } 221 222 /** 223 * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given 224 * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking 225 * on each option in isolation. 226 */ 227 static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) { 228 // Mind which fields have to preserve order. 229 Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>(); 230 Map<String, OptionDefinition> nameToFieldBuilder = new LinkedHashMap<>(); 231 Map<String, OptionDefinition> oldNameToFieldBuilder = new LinkedHashMap<>(); 232 Map<Character, OptionDefinition> abbrevToFieldBuilder = new HashMap<>(); 233 234 // Maps the negated boolean flag aliases to the original option name. 235 Map<String, String> booleanAliasMap = new HashMap<>(); 236 237 Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>(); 238 239 // Combine the option definitions for these options classes, and check that they do not 240 // conflict. The options are individually checked for correctness at compile time in the 241 // OptionProcessor. 242 for (Class<? extends OptionsBase> parsedOptionsClass : classes) { 243 try { 244 Constructor<? extends OptionsBase> constructor = parsedOptionsClass.getConstructor(); 245 constructorBuilder.put(parsedOptionsClass, constructor); 246 } catch (NoSuchMethodException e) { 247 throw new IllegalArgumentException(parsedOptionsClass 248 + " lacks an accessible default constructor"); 249 } 250 ImmutableList<OptionDefinition> optionDefinitions = 251 getAllOptionDefinitionsForClass(parsedOptionsClass); 252 253 for (OptionDefinition optionDefinition : optionDefinitions) { 254 try { 255 String optionName = optionDefinition.getOptionName(); 256 checkForCollisions(nameToFieldBuilder, optionName, "option name collision"); 257 checkForCollisions( 258 oldNameToFieldBuilder, 259 optionName, 260 "option name collision with another option's old name"); 261 checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option"); 262 if (optionDefinition.usesBooleanValueSyntax()) { 263 checkAndUpdateBooleanAliases( 264 nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, optionName); 265 } 266 nameToFieldBuilder.put(optionName, optionDefinition); 267 268 if (!optionDefinition.getOldOptionName().isEmpty()) { 269 String oldName = optionDefinition.getOldOptionName(); 270 checkForCollisions( 271 nameToFieldBuilder, 272 oldName, 273 "old option name collision with another option's canonical name"); 274 checkForCollisions( 275 oldNameToFieldBuilder, 276 oldName, 277 "old option name collision with another old option name"); 278 checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name"); 279 // If boolean, repeat the alias dance for the old name. 280 if (optionDefinition.usesBooleanValueSyntax()) { 281 checkAndUpdateBooleanAliases( 282 nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, oldName); 283 } 284 // Now that we've checked for conflicts, confidently store the old name. 285 oldNameToFieldBuilder.put(oldName, optionDefinition); 286 } 287 if (optionDefinition.getAbbreviation() != '\0') { 288 checkForCollisions( 289 abbrevToFieldBuilder, optionDefinition.getAbbreviation(), "option abbreviation"); 290 abbrevToFieldBuilder.put(optionDefinition.getAbbreviation(), optionDefinition); 291 } 292 } catch (DuplicateOptionDeclarationException e) { 293 throw new ConstructionException(e); 294 } 295 } 296 297 boolean usesOnlyCoreTypes = parsedOptionsClass.isAnnotationPresent(UsesOnlyCoreTypes.class); 298 if (usesOnlyCoreTypes) { 299 // Validate that @UsesOnlyCoreTypes was used correctly. 300 for (OptionDefinition optionDefinition : optionDefinitions) { 301 // The classes in coreTypes are all final. But even if they weren't, we only want to check 302 // for exact matches; subclasses would not be considered core types. 303 if (!UsesOnlyCoreTypes.CORE_TYPES.contains(optionDefinition.getType())) { 304 throw new ConstructionException( 305 "Options class '" 306 + parsedOptionsClass.getName() 307 + "' is marked as " 308 + "@UsesOnlyCoreTypes, but field '" 309 + optionDefinition.getField().getName() 310 + "' has type '" 311 + optionDefinition.getType().getName() 312 + "'"); 313 } 314 } 315 } 316 usesOnlyCoreTypesBuilder.put(parsedOptionsClass, usesOnlyCoreTypes); 317 } 318 319 return new IsolatedOptionsData( 320 constructorBuilder, 321 nameToFieldBuilder, 322 oldNameToFieldBuilder, 323 abbrevToFieldBuilder, 324 usesOnlyCoreTypesBuilder); 325 } 326 327 } 328