blob: 3a32412f690eb401afc7c400351949c4b3b67d25 [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.Iterables;
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.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import java.lang.reflect.Field;
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#category} must be "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}, {@link Option#oldName}, and {@link
* Option#wrapperOption}
* </ul>
*
* Example:
*
* <pre>{@code
* @Option(
* name = "incompatible_foo",
* category = "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 satisfy either the name or category requirement 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 unconditionally, it is recommended (required?) that
* its corresponding incompatible change option be left as a valid no-op option, rather than
* removed. This helps avoid breaking invocations of Bazel upon upgrading to a new release. Just as
* for other options, names of incompatible change options must never be reused for a different
* option.
*/
// 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_";
// The reserved category for all incompatible change options.
public static final String INCOMPATIBLE_CATEGORY = "incompatible changes";
/**
* 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(Field field, Option annotation) {
String prefix = "Incompatible change option '--" + annotation.name() + "' ";
// 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 (annotation.abbrev() != '\0') {
throw new IllegalArgumentException(prefix + "must not use the abbrev field");
}
if (!annotation.valueHelp().equals(defaultString)) {
throw new IllegalArgumentException(prefix + "must not use the valueHelp field");
}
if (annotation.converter() != Converter.class) {
throw new IllegalArgumentException(prefix + "must not use the converter field");
}
if (annotation.allowMultiple()) {
throw new IllegalArgumentException(prefix + "must not use the allowMultiple field");
}
if (annotation.implicitRequirements().length > 0) {
throw new IllegalArgumentException(prefix + "must not use the implicitRequirements field");
}
if (!annotation.oldName().equals(defaultString)) {
throw new IllegalArgumentException(prefix + "must not use the oldName field");
}
if (annotation.wrapperOption()) {
throw new IllegalArgumentException(prefix + "must not use the wrapperOption field");
}
// Validate the fields that are actually allowed.
if (!annotation.name().startsWith(INCOMPATIBLE_NAME_PREFIX)) {
throw new IllegalArgumentException(prefix + "must have name starting with \"incompatible_\"");
}
if (!annotation.category().equals(INCOMPATIBLE_CATEGORY)) {
throw new IllegalArgumentException(prefix + "must have category \"incompatible changes\"");
}
if (!IsolatedOptionsData.isExpansionOption(annotation)) {
if (!field.getType().equals(Boolean.TYPE)) {
throw new IllegalArgumentException(
prefix + "must have boolean type (unless it's an expansion option)");
}
}
if (annotation.help().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 String[] getExpansion(IsolatedOptionsData optionsData) {
// Grab all registered options that are identified as incompatible changes by either name or
// by category. Ensure they satisfy our requirements.
ArrayList<String> incompatibleChanges = new ArrayList<>();
for (Map.Entry<String, Field> entry : optionsData.getAllNamedFields()) {
Field field = entry.getValue();
Option annotation = field.getAnnotation(Option.class);
if (annotation.name().startsWith(INCOMPATIBLE_NAME_PREFIX)
|| annotation.category().equals(INCOMPATIBLE_CATEGORY)) {
validateIncompatibleChange(field, annotation);
incompatibleChanges.add("--" + annotation.name());
}
}
// 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 Iterables.toArray(incompatibleChanges, String.class);
}
}