blob: d0a73eea59ccbe82667cb0b64cdb2bbf666e5bd1 [file] [log] [blame]
// 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);
}
}