Home | History | Annotate | Download | only in options
      1 // Copyright 2015 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 package com.google.devtools.common.options;
     15 
     16 import com.google.common.base.Joiner;
     17 import com.google.common.base.Verify;
     18 import com.google.common.collect.ArrayListMultimap;
     19 import com.google.common.collect.ImmutableList;
     20 import com.google.common.collect.ImmutableSet;
     21 import com.google.common.collect.Multimap;
     22 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.AllowValues;
     23 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.DisallowValues;
     24 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy;
     25 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy.OperationCase;
     26 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
     27 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue;
     28 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault;
     29 import com.google.devtools.common.options.OptionPriority.PriorityCategory;
     30 import com.google.devtools.common.options.OptionsParser.OptionDescription;
     31 import java.util.ArrayList;
     32 import java.util.Collections;
     33 import java.util.HashMap;
     34 import java.util.HashSet;
     35 import java.util.List;
     36 import java.util.Map;
     37 import java.util.Set;
     38 import java.util.logging.Level;
     39 import java.util.logging.Logger;
     40 import java.util.stream.Collectors;
     41 import javax.annotation.Nullable;
     42 
     43 /**
     44  * Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an {@link
     45  * OptionsParser} by validating and changing the flag values in the given {@link OptionsParser}.
     46  *
     47  * <p>"Flag" and "Option" are used interchangeably in this file.
     48  */
     49 public final class InvocationPolicyEnforcer {
     50 
     51   private static final Logger logger = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
     52 
     53   private static final String INVOCATION_POLICY_SOURCE = "Invocation policy";
     54   @Nullable private final InvocationPolicy invocationPolicy;
     55   private final Level loglevel;
     56 
     57   /**
     58    * Creates an InvocationPolicyEnforcer that enforces the given policy.
     59    *
     60    * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
     61    *     nothing in calls to enforce().
     62    */
     63   public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) {
     64     this(invocationPolicy, Level.FINE);
     65   }
     66 
     67   /**
     68    * Creates an InvocationPolicyEnforcer that enforces the given policy.
     69    *
     70    * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
     71    *     nothing in calls to enforce().
     72    * @param loglevel the level at which to log informational statements. Warnings and errors will
     73    *     still be logged at the appropriate level.
     74    */
     75   public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy, Level loglevel) {
     76     this.invocationPolicy = invocationPolicy;
     77     this.loglevel = loglevel;
     78   }
     79 
     80   private static final class FlagPolicyWithContext {
     81     private final FlagPolicy policy;
     82     private final OptionDescription description;
     83     private final OptionInstanceOrigin origin;
     84 
     85     public FlagPolicyWithContext(
     86         FlagPolicy policy, OptionDescription description, OptionInstanceOrigin origin) {
     87       this.policy = policy;
     88       this.description = description;
     89       this.origin = origin;
     90     }
     91   }
     92 
     93   public InvocationPolicy getInvocationPolicy() {
     94     return invocationPolicy;
     95   }
     96 
     97   /**
     98    * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser for all blaze commands.
     99    *
    100    * @param parser The OptionsParser to enforce policy on.
    101    * @throws OptionsParsingException if any flag policy is invalid.
    102    */
    103   public void enforce(OptionsParser parser) throws OptionsParsingException {
    104     enforce(parser, null);
    105   }
    106 
    107   /**
    108    * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser.
    109    *
    110    * @param parser The OptionsParser to enforce policy on.
    111    * @param command The current blaze command, for flag policies that apply to only specific
    112    *     commands. Such policies will be enforced only if they contain this command or a command
    113    *     they inherit from
    114    * @throws OptionsParsingException if any flag policy is invalid.
    115    */
    116   public void enforce(OptionsParser parser, @Nullable String command)
    117       throws OptionsParsingException {
    118     if (invocationPolicy == null || invocationPolicy.getFlagPoliciesCount() == 0) {
    119       return;
    120     }
    121 
    122     // The effective policy returned is expanded, filtered for applicable commands, and cleaned of
    123     // redundancies and conflicts.
    124     List<FlagPolicyWithContext> effectivePolicies =
    125         getEffectivePolicies(invocationPolicy, parser, command, loglevel);
    126 
    127     for (FlagPolicyWithContext flagPolicy : effectivePolicies) {
    128       String flagName = flagPolicy.policy.getFlagName();
    129 
    130       OptionValueDescription valueDescription;
    131       try {
    132         valueDescription = parser.getOptionValueDescription(flagName);
    133       } catch (IllegalArgumentException e) {
    134         // This flag doesn't exist. We are deliberately lenient if the flag policy has a flag
    135         // we don't know about. This is for better future proofing so that as new flags are added,
    136         // new policies can use the new flags without worrying about older versions of Bazel.
    137         logger.log(
    138             loglevel,
    139             String.format("Flag '%s' specified by invocation policy does not exist", flagName));
    140         continue;
    141       }
    142 
    143       // getOptionDescription() will return null if the option does not exist, however
    144       // getOptionValueDescription() above would have thrown an IllegalArgumentException if that
    145       // were the case.
    146       Verify.verifyNotNull(flagPolicy.description);
    147 
    148       switch (flagPolicy.policy.getOperationCase()) {
    149         case SET_VALUE:
    150           applySetValueOperation(parser, flagPolicy, valueDescription, loglevel);
    151           break;
    152 
    153         case USE_DEFAULT:
    154           applyUseDefaultOperation(
    155               parser, "UseDefault", flagPolicy.description.getOptionDefinition(), loglevel);
    156           break;
    157 
    158         case ALLOW_VALUES:
    159           AllowValues allowValues = flagPolicy.policy.getAllowValues();
    160           FilterValueOperation.AllowValueOperation allowValueOperation =
    161               new FilterValueOperation.AllowValueOperation(loglevel);
    162           allowValueOperation.apply(
    163               parser,
    164               flagPolicy.origin,
    165               allowValues.getAllowedValuesList(),
    166               allowValues.hasNewValue() ? allowValues.getNewValue() : null,
    167               allowValues.hasUseDefault(),
    168               valueDescription,
    169               flagPolicy.description);
    170           break;
    171 
    172         case DISALLOW_VALUES:
    173           DisallowValues disallowValues = flagPolicy.policy.getDisallowValues();
    174           FilterValueOperation.DisallowValueOperation disallowValueOperation =
    175               new FilterValueOperation.DisallowValueOperation(loglevel);
    176           disallowValueOperation.apply(
    177               parser,
    178               flagPolicy.origin,
    179               disallowValues.getDisallowedValuesList(),
    180               disallowValues.hasNewValue() ? disallowValues.getNewValue() : null,
    181               disallowValues.hasUseDefault(),
    182               valueDescription,
    183               flagPolicy.description);
    184           break;
    185 
    186         case OPERATION_NOT_SET:
    187           throw new PolicyOperationNotSetException(flagName);
    188 
    189         default:
    190           logger.warning(
    191               String.format(
    192                   "Unknown operation '%s' from invocation policy for flag '%s'",
    193                   flagPolicy.policy.getOperationCase(), flagName));
    194           break;
    195       }
    196     }
    197   }
    198 
    199   private static class PolicyOperationNotSetException extends OptionsParsingException {
    200     PolicyOperationNotSetException(String flagName) {
    201       super(String.format("Flag policy for flag '%s' does not " + "have an operation", flagName));
    202     }
    203   }
    204 
    205   private static boolean policyApplies(FlagPolicy policy, ImmutableSet<String> applicableCommands) {
    206     // Skip the flag policy if it doesn't apply to this command. If the commands list is empty,
    207     // then the policy applies to all commands.
    208     if (policy.getCommandsList().isEmpty() || applicableCommands.isEmpty()) {
    209       return true;
    210     }
    211 
    212     return !Collections.disjoint(policy.getCommandsList(), applicableCommands);
    213   }
    214 
    215   /** Returns the expanded and filtered policy that would be enforced for the given command. */
    216   public static InvocationPolicy getEffectiveInvocationPolicy(
    217       InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
    218       throws OptionsParsingException {
    219     ImmutableList<FlagPolicyWithContext> effectivePolicies =
    220         getEffectivePolicies(invocationPolicy, parser, command, loglevel);
    221 
    222     InvocationPolicy.Builder builder = InvocationPolicy.newBuilder();
    223     for (FlagPolicyWithContext policyWithContext : effectivePolicies) {
    224       builder.addFlagPolicies(policyWithContext.policy);
    225     }
    226     return builder.build();
    227   }
    228 
    229   /**
    230    * Takes the provided policy and processes it to the form that can be used on the user options.
    231    *
    232    * <p>Expands any policies on expansion flags.
    233    */
    234   private static ImmutableList<FlagPolicyWithContext> getEffectivePolicies(
    235       InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
    236       throws OptionsParsingException {
    237     if (invocationPolicy == null) {
    238       return ImmutableList.of();
    239     }
    240 
    241     ImmutableSet<String> commandAndParentCommands =
    242         command == null
    243             ? ImmutableSet.of()
    244             : CommandNameCache.CommandNameCacheInstance.INSTANCE.get(command);
    245 
    246     // Expand all policies to transfer policies on expansion flags to policies on the child flags.
    247     List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
    248     OptionPriority nextPriority =
    249         OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY);
    250     for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) {
    251       // Explicitly disallow --config in invocation policy.
    252       if (policy.getFlagName().equals("config")) {
    253         throw new OptionsParsingException(
    254             "Invocation policy is applied after --config expansion, changing config values now "
    255                 + "would have no effect and is disallowed to prevent confusion. Please remove the "
    256                 + "following policy : "
    257                 + policy);
    258       }
    259 
    260       // These policies are high-level, before expansion, and so are not the implicitDependents or
    261       // expansions of any other flag, other than in an obtuse sense from --invocation_policy.
    262       OptionPriority currentPriority = nextPriority;
    263       OptionInstanceOrigin origin =
    264           new OptionInstanceOrigin(currentPriority, INVOCATION_POLICY_SOURCE, null, null);
    265       nextPriority = OptionPriority.nextOptionPriority(currentPriority);
    266       if (!policyApplies(policy, commandAndParentCommands)) {
    267         // Only keep and expand policies that are applicable to the current command.
    268         continue;
    269       }
    270 
    271       OptionDescription optionDescription = parser.getOptionDescription(policy.getFlagName());
    272       if (optionDescription == null) {
    273         // InvocationPolicy ignores policy on non-existing flags by design, for version
    274         // compatibility.
    275         logger.log(
    276             loglevel,
    277             String.format(
    278                 "Flag '%s' specified by invocation policy does not exist, and will be ignored",
    279                 policy.getFlagName()));
    280         continue;
    281       }
    282       FlagPolicyWithContext policyWithContext =
    283           new FlagPolicyWithContext(policy, optionDescription, origin);
    284       List<FlagPolicyWithContext> policies = expandPolicy(policyWithContext, parser, loglevel);
    285       expandedPolicies.addAll(policies);
    286     }
    287 
    288     // Only keep that last policy for each flag.
    289     Map<String, FlagPolicyWithContext> effectivePolicy = new HashMap<>();
    290     for (FlagPolicyWithContext expandedPolicy : expandedPolicies) {
    291       String flagName = expandedPolicy.policy.getFlagName();
    292       effectivePolicy.put(flagName, expandedPolicy);
    293     }
    294 
    295     return ImmutableList.copyOf(effectivePolicy.values());
    296   }
    297 
    298   private static void throwAllowValuesOnExpansionFlagException(String flagName)
    299       throws OptionsParsingException {
    300     throw new OptionsParsingException(
    301         String.format("Allow_Values on expansion flags like %s is not allowed.", flagName));
    302   }
    303 
    304   private static void throwDisallowValuesOnExpansionFlagException(String flagName)
    305       throws OptionsParsingException {
    306     throw new OptionsParsingException(
    307         String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName));
    308   }
    309 
    310   /**
    311    * Expand a single policy. If the policy is not about an expansion flag, this will simply return a
    312    * list with a single element, oneself. If the policy is for an expansion flag, the policy will
    313    * get split into multiple policies applying to each flag the original flag expands to.
    314    *
    315    * <p>None of the flagPolicies returned should be on expansion flags.
    316    */
    317   private static List<FlagPolicyWithContext> expandPolicy(
    318       FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel)
    319       throws OptionsParsingException {
    320     List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
    321 
    322     boolean isExpansion = originalPolicy.description.isExpansion();
    323     ImmutableList<ParsedOptionDescription> subflags =
    324         parser.getExpansionValueDescriptions(
    325             originalPolicy.description.getOptionDefinition(), originalPolicy.origin);
    326 
    327     // If we have nothing to expand to, no need to do any further work.
    328     if (subflags.isEmpty()) {
    329       return ImmutableList.of(originalPolicy);
    330     }
    331 
    332     if (logger.isLoggable(loglevel)) {
    333       // Log the expansion. This is only really useful for understanding the invocation policy
    334       // itself.
    335       List<String> subflagNames = new ArrayList<>(subflags.size());
    336       for (ParsedOptionDescription subflag : subflags) {
    337         subflagNames.add("--" + subflag.getOptionDefinition().getOptionName());
    338       }
    339 
    340       logger.logp(
    341           loglevel,
    342           "InvocationPolicyEnforcer",
    343           "expandPolicy",
    344           String.format(
    345               "Expanding %s on option %s to its %s: %s.",
    346               originalPolicy.policy.getOperationCase(),
    347               originalPolicy.policy.getFlagName(),
    348               isExpansion ? "expansions" : "implied flags",
    349               Joiner.on("; ").join(subflagNames)));
    350     }
    351 
    352     // Repeated flags are special, and could set multiple times in an expansion, with the user
    353     // expecting both values to be valid. Collect these separately.
    354     Multimap<OptionDescription, ParsedOptionDescription> repeatableSubflagsInSetValues =
    355         ArrayListMultimap.create();
    356 
    357     // Create a flag policy for the child that looks like the parent's policy "transferred" to its
    358     // child. Note that this only makes sense for SetValue, when setting an expansion flag, or
    359     // UseDefault, when preventing it from being set.
    360     for (ParsedOptionDescription currentSubflag : subflags) {
    361       OptionDescription subflagOptionDescription =
    362           parser.getOptionDescription(currentSubflag.getOptionDefinition().getOptionName());
    363 
    364       if (currentSubflag.getOptionDefinition().allowsMultiple()
    365           && originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)) {
    366         repeatableSubflagsInSetValues.put(subflagOptionDescription, currentSubflag);
    367       } else {
    368         FlagPolicyWithContext subflagAsPolicy =
    369             getSingleValueSubflagAsPolicy(
    370                 subflagOptionDescription, currentSubflag, originalPolicy, isExpansion);
    371         // In case any of the expanded flags are themselves expansions, recurse.
    372         expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser, loglevel));
    373       }
    374     }
    375 
    376     // If there are any repeatable flag SetValues, deal with them together now.
    377     // Note that expansion flags have no value, and so cannot have multiple values either.
    378     // Skipping the recursion above is fine.
    379     for (OptionDescription repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
    380       int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size();
    381       ArrayList<String> newValues = new ArrayList<>(numValues);
    382       ArrayList<OptionInstanceOrigin> origins = new ArrayList<>(numValues);
    383       for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) {
    384         newValues.add(setValue.getUnconvertedValue());
    385         origins.add(setValue.getOrigin());
    386       }
    387       // These options come from expanding a single policy, so they have effectively the same
    388       // priority. They could have come from different expansions or implicit requirements in the
    389       // recursive resolving of the option list, so just pick the first one. Do collapse the source
    390       // strings though, in case there are different sources.
    391       OptionInstanceOrigin arbitraryFirstOptionOrigin = origins.get(0);
    392       OptionInstanceOrigin originOfSubflags =
    393           new OptionInstanceOrigin(
    394               arbitraryFirstOptionOrigin.getPriority(),
    395               origins
    396                   .stream()
    397                   .map(OptionInstanceOrigin::getSource)
    398                   .distinct()
    399                   .collect(Collectors.joining(", ")),
    400               arbitraryFirstOptionOrigin.getImplicitDependent(),
    401               arbitraryFirstOptionOrigin.getExpandedFrom());
    402       expandedPolicies.add(
    403           getSetValueSubflagAsPolicy(repeatableFlag, newValues, originOfSubflags, originalPolicy));
    404     }
    405 
    406     // Don't add the original policy if it was an expansion flag, which have no value, but do add
    407     // it if there was either no expansion or if it was a valued flag with implicit requirements.
    408     if (!isExpansion) {
    409       expandedPolicies.add(originalPolicy);
    410     }
    411 
    412     return expandedPolicies;
    413   }
    414 
    415   /**
    416    * Expand a SetValue flag policy on a repeatable flag. SetValue operations are the only flag
    417    * policies that set the flag, and so interact with repeatable flags, flags that can be set
    418    * multiple times, in subtle ways.
    419    *
    420    * @param subflagDesc, the description of the flag the SetValue'd expansion flag expands to.
    421    * @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag.
    422    * @param originalPolicy, the original policy on the expansion flag.
    423    * @return the flag policy for the subflag given, this will be part of the expanded form of the
    424    *     SetValue policy on the original flag.
    425    */
    426   private static FlagPolicyWithContext getSetValueSubflagAsPolicy(
    427       OptionDescription subflagDesc,
    428       List<String> subflagValue,
    429       OptionInstanceOrigin subflagOrigin,
    430       FlagPolicyWithContext originalPolicy) {
    431     // Some sanity checks.
    432     OptionDefinition subflag = subflagDesc.getOptionDefinition();
    433     Verify.verify(originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE));
    434     if (!subflag.allowsMultiple()) {
    435       Verify.verify(subflagValue.size() <= 1);
    436     }
    437 
    438     // Flag value from the expansion, overridability from the original policy, unless the flag is
    439     // repeatable, in which case we care about appendability, not overridability.
    440     SetValue.Builder setValueExpansion = SetValue.newBuilder();
    441     for (String value : subflagValue) {
    442       setValueExpansion.addFlagValue(value);
    443     }
    444     if (subflag.allowsMultiple()) {
    445       setValueExpansion.setAppend(originalPolicy.policy.getSetValue().getOverridable());
    446     } else {
    447       setValueExpansion.setOverridable(originalPolicy.policy.getSetValue().getOverridable());
    448     }
    449 
    450     // Commands from the original policy, flag name of the expansion
    451     return new FlagPolicyWithContext(
    452         FlagPolicy.newBuilder()
    453             .addAllCommands(originalPolicy.policy.getCommandsList())
    454             .setFlagName(subflag.getOptionName())
    455             .setSetValue(setValueExpansion)
    456             .build(),
    457         subflagDesc,
    458         subflagOrigin);
    459   }
    460 
    461   /**
    462    * For an expansion flag in an invocation policy, each flag it expands to must be given a
    463    * corresponding policy.
    464    */
    465   private static FlagPolicyWithContext getSingleValueSubflagAsPolicy(
    466       OptionDescription subflagContext,
    467       ParsedOptionDescription currentSubflag,
    468       FlagPolicyWithContext originalPolicy,
    469       boolean isExpansion)
    470       throws OptionsParsingException {
    471     FlagPolicyWithContext subflagAsPolicy = null;
    472     switch (originalPolicy.policy.getOperationCase()) {
    473       case SET_VALUE:
    474         if (currentSubflag.getOptionDefinition().allowsMultiple()) {
    475           throw new AssertionError(
    476               "SetValue subflags with allowMultiple should have been dealt with separately and "
    477                   + "accumulated into a single FlagPolicy.");
    478         }
    479         // Accept null originalValueStrings, they are expected when the subflag is also an expansion
    480         // flag.
    481         List<String> subflagValue;
    482         if (currentSubflag.getUnconvertedValue() == null) {
    483           subflagValue = ImmutableList.of();
    484         } else {
    485           subflagValue = ImmutableList.of(currentSubflag.getUnconvertedValue());
    486         }
    487         subflagAsPolicy =
    488             getSetValueSubflagAsPolicy(
    489                 subflagContext, subflagValue, currentSubflag.getOrigin(), originalPolicy);
    490         break;
    491 
    492       case USE_DEFAULT:
    493         // Commands from the original policy, flag name of the expansion
    494         subflagAsPolicy =
    495             new FlagPolicyWithContext(
    496                 FlagPolicy.newBuilder()
    497                     .addAllCommands(originalPolicy.policy.getCommandsList())
    498                     .setFlagName(currentSubflag.getOptionDefinition().getOptionName())
    499                     .setUseDefault(UseDefault.getDefaultInstance())
    500                     .build(),
    501                 subflagContext,
    502                 currentSubflag.getOrigin());
    503         break;
    504 
    505       case ALLOW_VALUES:
    506         if (isExpansion) {
    507           throwAllowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
    508         }
    509         // If this flag is an implicitRequirement, and some values for the parent flag are
    510         // allowed, nothing needs to happen on the implicitRequirement that is set for all
    511         // values of the flag.
    512         break;
    513 
    514       case DISALLOW_VALUES:
    515         if (isExpansion) {
    516           throwDisallowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
    517         }
    518         // If this flag is an implicitRequirement, and some values for the parent flag are
    519         // disallowed, that implies that all others are allowed, so nothing needs to happen
    520         // on the implicitRequirement that is set for all values of the parent flag.
    521         break;
    522 
    523       case OPERATION_NOT_SET:
    524         throw new PolicyOperationNotSetException(originalPolicy.policy.getFlagName());
    525 
    526       default:
    527         return null;
    528     }
    529     return subflagAsPolicy;
    530   }
    531 
    532   private static void logInApplySetValueOperation(
    533       Level loglevel, String formattingString, Object... objects) {
    534     // Finding the caller here is relatively expensive and shows up in profiling, so provide it
    535     // manually.
    536     logger.logp(
    537         loglevel,
    538         "InvocationPolicyEnforcer",
    539         "applySetValueOperation",
    540         String.format(formattingString, objects));
    541   }
    542 
    543   private static void applySetValueOperation(
    544       OptionsParser parser,
    545       FlagPolicyWithContext flagPolicy,
    546       OptionValueDescription valueDescription,
    547       Level loglevel)
    548       throws OptionsParsingException {
    549     SetValue setValue = flagPolicy.policy.getSetValue();
    550     OptionDefinition optionDefinition = flagPolicy.description.getOptionDefinition();
    551 
    552     // SetValue.flag_value must have at least 1 value.
    553     if (setValue.getFlagValueCount() == 0) {
    554       throw new OptionsParsingException(
    555           String.format(
    556               "SetValue operation from invocation policy for %s does not have a value",
    557               optionDefinition));
    558     }
    559 
    560     // Flag must allow multiple values if multiple values are specified by the policy.
    561     if (setValue.getFlagValueCount() > 1
    562         && !flagPolicy.description.getOptionDefinition().allowsMultiple()) {
    563       throw new OptionsParsingException(
    564           String.format(
    565               "SetValue operation from invocation policy sets multiple values for %s which "
    566                   + "does not allow multiple values",
    567               optionDefinition));
    568     }
    569 
    570     if (setValue.getOverridable() && valueDescription != null) {
    571       // The user set the value for the flag but the flag policy is overridable, so keep the user's
    572       // value.
    573       logInApplySetValueOperation(
    574           loglevel,
    575           "Keeping value '%s' from source '%s' for %s because the invocation policy specifying "
    576               + "the value(s) '%s' is overridable",
    577           valueDescription.getValue(),
    578           valueDescription.getSourceString(),
    579           optionDefinition,
    580           setValue.getFlagValueList());
    581     } else {
    582 
    583       if (!setValue.getAppend()) {
    584         // Clear the value in case the flag is a repeated flag so that values don't accumulate.
    585         parser.clearValue(flagPolicy.description.getOptionDefinition());
    586       }
    587 
    588       // Set all the flag values from the policy.
    589       for (String flagValue : setValue.getFlagValueList()) {
    590         if (valueDescription == null) {
    591           logInApplySetValueOperation(
    592               loglevel,
    593               "Setting value for %s from invocation policy to '%s', overriding the default value "
    594                   + "'%s'",
    595               optionDefinition,
    596               flagValue,
    597               optionDefinition.getDefaultValue());
    598         } else {
    599           logInApplySetValueOperation(
    600               loglevel,
    601               "Setting value for %s from invocation policy to '%s', overriding value '%s' from "
    602                   + "'%s'",
    603               optionDefinition,
    604               flagValue,
    605               valueDescription.getValue(),
    606               valueDescription.getSourceString());
    607         }
    608 
    609         parser.addOptionValueAtSpecificPriority(flagPolicy.origin, optionDefinition, flagValue);
    610       }
    611     }
    612   }
    613 
    614   private static void applyUseDefaultOperation(
    615       OptionsParser parser, String policyType, OptionDefinition option, Level loglevel)
    616       throws OptionsParsingException {
    617     OptionValueDescription clearedValueDescription = parser.clearValue(option);
    618     if (clearedValueDescription != null) {
    619       // Log the removed value.
    620       String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName();
    621       Object clearedFlagDefaultValue =
    622           clearedValueDescription.getOptionDefinition().getDefaultValue();
    623       logger.log(
    624           loglevel,
    625           String.format(
    626               "Using default value '%s' for flag '%s' as specified by %s invocation policy, "
    627                   + "overriding original value '%s' from '%s'",
    628               clearedFlagDefaultValue,
    629               clearedFlagName,
    630               policyType,
    631               clearedValueDescription.getValue(),
    632               clearedValueDescription.getSourceString()));
    633     }
    634   }
    635 
    636   /** Checks the user's flag values against a filtering function. */
    637   private abstract static class FilterValueOperation {
    638 
    639     private static final class AllowValueOperation extends FilterValueOperation {
    640       AllowValueOperation(Level loglevel) {
    641         super("Allow", loglevel);
    642       }
    643 
    644       @Override
    645       boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
    646         return convertedPolicyValues.contains(value);
    647       }
    648     }
    649 
    650     private static final class DisallowValueOperation extends FilterValueOperation {
    651       DisallowValueOperation(Level loglevel) {
    652         super("Disalllow", loglevel);
    653       }
    654 
    655       @Override
    656       boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
    657         // In a disallow operation, the values that the flag policy specifies are not allowed,
    658         // so the value is allowed if the set of policy values does not contain the current
    659         // flag value.
    660         return !convertedPolicyValues.contains(value);
    661       }
    662     }
    663 
    664     private final String policyType;
    665     private final Level loglevel;
    666 
    667     FilterValueOperation(String policyType, Level loglevel) {
    668       this.policyType = policyType;
    669       this.loglevel = loglevel;
    670     }
    671 
    672     /**
    673      * Determines if the given value is allowed.
    674      *
    675      * @param convertedPolicyValues The values given from the FlagPolicy, converted to real objects.
    676      * @param value The user value of the flag.
    677      * @return True if the value should be allowed, false if it should not.
    678      */
    679     abstract boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value);
    680 
    681     void apply(
    682         OptionsParser parser,
    683         OptionInstanceOrigin origin,
    684         List<String> policyValues,
    685         String newValue,
    686         boolean useDefault,
    687         OptionValueDescription valueDescription,
    688         OptionDescription optionDescription)
    689         throws OptionsParsingException {
    690       OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
    691       // Convert all the allowed values from strings to real objects using the options'
    692       // converters so that they can be checked for equality using real .equals() instead
    693       // of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-"
    694       // (if the option has an abbreviation) are all equal for boolean flags. Plus converters
    695       // can be arbitrarily complex.
    696       Set<Object> convertedPolicyValues = new HashSet<>();
    697       for (String value : policyValues) {
    698         Object convertedValue = optionDefinition.getConverter().convert(value);
    699         // Some converters return lists, and if the flag is a repeatable flag, the items in the
    700         // list from the converter should be added, and not the list itself. Otherwise the items
    701         // from invocation policy will be compared to lists, which will never work.
    702         // See OptionsParserImpl.ParsedOptionEntry.addValue.
    703         if (optionDefinition.allowsMultiple() && convertedValue instanceof List<?>) {
    704           convertedPolicyValues.addAll((List<?>) convertedValue);
    705         } else {
    706           convertedPolicyValues.add(optionDefinition.getConverter().convert(value));
    707         }
    708       }
    709 
    710       // Check that if the default value of the flag is disallowed by the policy, that the policy
    711       // does not also set use_default. Otherwise the default value would still be set if the
    712       // user uses a disallowed value. This doesn't apply to repeatable flags since the default
    713       // value for repeatable flags is always the empty list. It also doesn't apply to flags that
    714       // are null by default, since these flags' default value is not parsed by the converter, so
    715       // there is no guarantee that there exists an accepted user-input value that would also set
    716       // the value to NULL. In these cases, we assume that "unset" is a distinct value that is
    717       // always allowed.
    718       if (!optionDescription.getOptionDefinition().allowsMultiple()
    719           && !optionDescription.getOptionDefinition().isSpecialNullDefault()) {
    720         boolean defaultValueAllowed =
    721             isFlagValueAllowed(
    722                 convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue());
    723         if (!defaultValueAllowed && useDefault) {
    724           throw new OptionsParsingException(
    725               String.format(
    726                   "%sValues policy disallows the default value '%s' for %s but also specifies to "
    727                       + "use the default value",
    728                   policyType, optionDefinition.getDefaultValue(), optionDefinition));
    729         }
    730       }
    731 
    732       if (valueDescription == null) {
    733         // Nothing has set the value yet, so check that the default value from the flag's
    734         // definition is allowed. The else case below (i.e. valueDescription is not null) checks for
    735         // the flag allowing multiple values, however, flags that allow multiple values cannot have
    736         // default values, and their value is always the empty list if they haven't been specified,
    737         // which is why new_default_value is not a repeated field.
    738         checkDefaultValue(
    739             parser, origin, optionDescription, policyValues, newValue, convertedPolicyValues);
    740       } else {
    741         checkUserValue(
    742             parser,
    743             origin,
    744             optionDescription,
    745             valueDescription,
    746             policyValues,
    747             newValue,
    748             useDefault,
    749             convertedPolicyValues);
    750       }
    751     }
    752 
    753     void checkDefaultValue(
    754         OptionsParser parser,
    755         OptionInstanceOrigin origin,
    756         OptionDescription optionDescription,
    757         List<String> policyValues,
    758         String newValue,
    759         Set<Object> convertedPolicyValues)
    760         throws OptionsParsingException {
    761 
    762       OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
    763       if (optionDefinition.isSpecialNullDefault()) {
    764         // Do nothing, the unset value by definition cannot be set. In option filtering operations,
    765         // the value is being filtered, but the value that is `no value` passes any filter.
    766         // Otherwise, there is no way to "usedefault" on one of these options that has no value by
    767         // default.
    768       } else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) {
    769         if (newValue != null) {
    770           // Use the default value from the policy, since the original default is not allowed
    771           logger.log(
    772               loglevel,
    773               String.format(
    774                   "Overriding default value '%s' for %s with value '%s' specified by invocation "
    775                       + "policy. %sed values are: %s",
    776                   optionDefinition.getDefaultValue(),
    777                   optionDefinition,
    778                   newValue,
    779                   policyType,
    780                   policyValues));
    781           parser.clearValue(optionDefinition);
    782           parser.addOptionValueAtSpecificPriority(origin, optionDefinition, newValue);
    783         } else {
    784           // The operation disallows the default value, but doesn't supply a new value.
    785           throw new OptionsParsingException(
    786               String.format(
    787                   "Default flag value '%s' for %s is not allowed by invocation policy, but "
    788                       + "the policy does not provide a new value. %sed values are: %s",
    789                   optionDescription.getOptionDefinition().getDefaultValue(),
    790                   optionDefinition,
    791                   policyType,
    792                   policyValues));
    793         }
    794       }
    795     }
    796 
    797     void checkUserValue(
    798         OptionsParser parser,
    799         OptionInstanceOrigin origin,
    800         OptionDescription optionDescription,
    801         OptionValueDescription valueDescription,
    802         List<String> policyValues,
    803         String newValue,
    804         boolean useDefault,
    805         Set<Object> convertedPolicyValues)
    806         throws OptionsParsingException {
    807       OptionDefinition option = optionDescription.getOptionDefinition();
    808       if (optionDescription.getOptionDefinition().allowsMultiple()) {
    809         // allowMultiple requires that the type of the option be List<T>, so cast from Object
    810         // to List<?>.
    811         List<?> optionValues = (List<?>) valueDescription.getValue();
    812         for (Object value : optionValues) {
    813           if (!isFlagValueAllowed(convertedPolicyValues, value)) {
    814             if (useDefault) {
    815               applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
    816             } else {
    817               throw new OptionsParsingException(
    818                   String.format(
    819                       "Flag value '%s' for %s is not allowed by invocation policy. %sed values "
    820                           + "are: %s",
    821                       value, option, policyType, policyValues));
    822             }
    823           }
    824         }
    825 
    826       } else {
    827 
    828         if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) {
    829           if (newValue != null) {
    830             logger.log(
    831                 loglevel,
    832                 String.format(
    833                     "Overriding disallowed value '%s' for %s with value '%s' "
    834                         + "specified by invocation policy. %sed values are: %s",
    835                     valueDescription.getValue(), option, newValue, policyType, policyValues));
    836             parser.clearValue(option);
    837             parser.addOptionValueAtSpecificPriority(origin, option, newValue);
    838           } else if (useDefault) {
    839             applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
    840           } else {
    841             throw new OptionsParsingException(
    842                 String.format(
    843                     "Flag value '%s' for %s is not allowed by invocation policy and the "
    844                         + "policy does not specify a new value. %sed values are: %s",
    845                     valueDescription.getValue(), option, policyType, policyValues));
    846           }
    847         }
    848       }
    849     }
    850   }
    851 }
    852