| // 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); |
| } |
| } |