| // Copyright 2017 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.build.lib.runtime; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.devtools.common.options.Converter; | |
| import com.google.devtools.common.options.ExpansionFunction; | |
| import com.google.devtools.common.options.IsolatedOptionsData; | |
| import com.google.devtools.common.options.Option; | |
| import com.google.devtools.common.options.OptionDefinition; | |
| import com.google.devtools.common.options.OptionMetadataTag; | |
| import com.google.devtools.common.options.OptionsBase; | |
| import com.google.devtools.common.options.OptionsParser; | |
| import java.util.ArrayList; | |
| import java.util.Map; | |
| /** | |
| * Expansion function for {@code --all_incompatible_changes}. Expands to all options of form {@code | |
| * --incompatible_*} that are declared in the {@link OptionsBase} subclasses that are passed to the | |
| * parser. | |
| * | |
| * <p>The incompatible changes system provides users with a uniform way of opting into backwards- | |
| * incompatible changes, in order to test whether their builds will be broken by an upcoming | |
| * release. When adding a new breaking change to Bazel, prefer to use this mechanism for guarding | |
| * the behavior. | |
| * | |
| * <p>An {@link Option}-annotated field that is considered an incompatible change must satisfy the | |
| * following requirements. | |
| * | |
| * <ul> | |
| * <li>the {@link Option#name} must be prefixed with "incompatible_" | |
| * <li>the {@link Option#metadataTags()} must include {@link | |
| * OptionMetadataTag#INCOMPATIBLE_CHANGE} and {@link | |
| * OptionMetadataTag#TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES} | |
| * <li>the {@link Option#help} field must be set, and must refer the user to information about | |
| * what the change does and how to migrate their code | |
| * <li>the following fields may not be used: {@link Option#abbrev}, {@link Option#valueHelp}, | |
| * {@link Option#converter}, {@link Option#allowMultiple}, and {@link Option#oldName} | |
| * </ul> | |
| * | |
| * Example: | |
| * | |
| * <pre>{@code | |
| * @Option( | |
| * name = "incompatible_foo", | |
| * metadataTags = { | |
| * OptionMetadataTag.INCOMPATIBLE_CHANGE, | |
| * OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES | |
| * }, | |
| * defaultValue = "false", | |
| * help = "Deprecates bar and changes the semantics of baz. To migrate your code see [...].") | |
| * public boolean incompatibleFoo; | |
| * }</pre> | |
| * | |
| * All options that have either the "incompatible_" prefix or the tag {@link | |
| * OptionMetadataTag#TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES} will be validated using the above | |
| * criteria. Any failure will cause {@link IllegalArgumentException} to be thrown, which will cause | |
| * the construction of the {@link OptionsParser} to fail with the <i>unchecked</i> exception {@link | |
| * OptionsParser.ConstructionException}. Therefore, when adding a new incompatible change, be aware | |
| * that an error in the specification of the {@code @Option} will exercise failure code paths in the | |
| * early part of the Bazel server execution. | |
| * | |
| * <p>After the breaking change has been enabled by default, it is recommended (required?) that the | |
| * flag stick around for a few releases, to provide users the flexibility to opt out. Even after | |
| * enabling the behavior unconditionally, it can still be useful to keep the flag around as a valid | |
| * no-op so that Bazel invocations are not immediately broken. | |
| * | |
| * <p>Generally speaking, we should never reuse names for multiple options. Therefore, when choosing | |
| * a name for a new incompatible change, try to describe not just the affected feature, but what the | |
| * change to that feature is. This avoids conflicts in case the feature changes multiple times. For | |
| * example, {@code "--incompatible_depset_constructor"} is ambiguous because it only communicates | |
| * that there is a change to how depsets are constructed, but {@code | |
| * "--incompatible_disallow_set_constructor"} uniquely says that the {@code set} alias for the | |
| * depset constructor is being disallowed. | |
| */ | |
| // Javadoc can't resolve inner classes. | |
| @SuppressWarnings("javadoc") | |
| public class AllIncompatibleChangesExpansion implements ExpansionFunction { | |
| // The reserved prefix for all incompatible change option names. | |
| public static final String INCOMPATIBLE_NAME_PREFIX = "incompatible_"; | |
| /** | |
| * Ensures that the given option satisfies all the requirements on incompatible change options | |
| * enumerated above. | |
| * | |
| * <p>If any of these requirements are not satisfied, {@link IllegalArgumentException} is thrown, | |
| * as this constitutes an internal error in the declaration of the option. | |
| */ | |
| private static void validateIncompatibleChange(OptionDefinition optionDefinition) { | |
| String prefix = String.format("Incompatible change %s ", optionDefinition); | |
| // To avoid ambiguity, and the suggestion of using .isEmpty(). | |
| String defaultString = ""; | |
| // Validate that disallowed fields aren't used. These will need updating if the default values | |
| // in Option ever change, and perhaps if new fields are added. | |
| if (optionDefinition.getAbbreviation() != '\0') { | |
| throw new IllegalArgumentException(prefix + "must not use the abbrev field"); | |
| } | |
| if (!optionDefinition.getValueTypeHelpText().equals(defaultString)) { | |
| throw new IllegalArgumentException(prefix + "must not use the valueHelp field"); | |
| } | |
| if (optionDefinition.getProvidedConverter() != Converter.class) { | |
| throw new IllegalArgumentException(prefix + "must not use the converter field"); | |
| } | |
| if (optionDefinition.allowsMultiple()) { | |
| throw new IllegalArgumentException(prefix + "must not use the allowMultiple field"); | |
| } | |
| if (optionDefinition.hasImplicitRequirements()) { | |
| throw new IllegalArgumentException(prefix + "must not use the implicitRequirements field"); | |
| } | |
| if (!optionDefinition.getOldOptionName().equals(defaultString) | |
| && !optionDefinition.getOldOptionName().startsWith("experimental_")) { | |
| throw new IllegalArgumentException(prefix + "must not use the oldName field"); | |
| } | |
| // Validate the fields that are actually allowed. | |
| if (!optionDefinition.getOptionName().startsWith(INCOMPATIBLE_NAME_PREFIX)) { | |
| throw new IllegalArgumentException(prefix + "must have name starting with \"incompatible_\""); | |
| } | |
| if (!ImmutableList.copyOf(optionDefinition.getOptionMetadataTags()) | |
| .contains(OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES)) { | |
| throw new IllegalArgumentException( | |
| prefix | |
| + "must have metadata tag OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES"); | |
| } | |
| if (!ImmutableList.copyOf(optionDefinition.getOptionMetadataTags()) | |
| .contains(OptionMetadataTag.INCOMPATIBLE_CHANGE)) { | |
| throw new IllegalArgumentException( | |
| prefix + "must have metadata tag OptionMetadataTag.INCOMPATIBLE_CHANGE"); | |
| } | |
| if (!optionDefinition.isExpansionOption()) { | |
| if (!optionDefinition.getType().equals(Boolean.TYPE)) { | |
| throw new IllegalArgumentException( | |
| prefix + "must have boolean type (unless it's an expansion option)"); | |
| } | |
| } | |
| if (optionDefinition.getHelpText().equals(defaultString)) { | |
| throw new IllegalArgumentException( | |
| prefix | |
| + "must have a \"help\" string that refers the user to " | |
| + "information about this change and how to migrate their code"); | |
| } | |
| } | |
| @Override | |
| public ImmutableList<String> getExpansion(IsolatedOptionsData optionsData) { | |
| // Grab all registered options that are identified as incompatible changes by either name or | |
| // by OptionMetadataTag. Ensure they satisfy our requirements. | |
| ArrayList<String> incompatibleChanges = new ArrayList<>(); | |
| for (Map.Entry<String, OptionDefinition> entry : optionsData.getAllOptionDefinitions()) { | |
| OptionDefinition optionDefinition = entry.getValue(); | |
| if (optionDefinition.getOptionName().startsWith(INCOMPATIBLE_NAME_PREFIX) | |
| || ImmutableList.copyOf(optionDefinition.getOptionMetadataTags()) | |
| .contains(OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES)) { | |
| validateIncompatibleChange(optionDefinition); | |
| incompatibleChanges.add("--" + optionDefinition.getOptionName()); | |
| } | |
| } | |
| // Sort to get a deterministic canonical order. This probably isn't necessary because the | |
| // options parser will do its own sorting when canonicalizing, but it seems like it can't hurt. | |
| incompatibleChanges.sort(null); | |
| return ImmutableList.copyOf(incompatibleChanges); | |
| } | |
| } |