com.google.devtools.build.lib.runtime.AllIncompatibleChangesExpansion.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.runtime.AllIncompatibleChangesExpansion.java

Source

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