diff options
Diffstat (limited to 'src/main/java/com/google')
7 files changed, 588 insertions, 98 deletions
diff --git a/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java index 52f34478eb..6c6b4f35bd 100644 --- a/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java +++ b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java @@ -32,7 +32,7 @@ public class BuildEncyclopediaGenerator { + "The product name (-n), rule class provider (-p) and at least one input_dir\n" + "(-i) must be specified.\n"); System.err.println( - parser.describeOptions( + parser.describeOptionsWithDeprecatedCategories( Collections.<String, String>emptyMap(), OptionsParser.HelpVerbosity.LONG)); } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java index 5f57b735bd..07573fa3a8 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java @@ -21,7 +21,6 @@ import com.google.devtools.build.lib.syntax.SkylarkSemanticsOptions; import com.google.devtools.build.lib.util.ResourceFileLoader; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; - import java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -114,21 +113,42 @@ public class BlazeCommandUtils { * Returns the expansion of the specified help topic. * * @param topic the name of the help topic; used in %{command} expansion. - * @param help the text template of the help message. Certain %{x} variables - * will be expanded. A prefix of "resource:" means use the .jar - * resource of that name. - * @param categoryDescriptions a mapping from option category names to - * descriptions, passed to {@link OptionsParser#describeOptions}. - * @param helpVerbosity a tri-state verbosity option selecting between just - * names, names and syntax, and full description. + * @param help the text template of the help message. Certain %{x} variables will be expanded. A + * prefix of "resource:" means use the .jar resource of that name. + * @param categoryDescriptions a mapping from option category names to descriptions, passed to + * {@link OptionsParser#describeOptionsWithDeprecatedCategories}. + * @param helpVerbosity a tri-state verbosity option selecting between just names, names and + * syntax, and full description. * @param productName the product name */ - public static String expandHelpTopic(String topic, String help, + public static final String expandHelpTopic( + String topic, + String help, Class<? extends BlazeCommand> commandClass, Collection<Class<? extends OptionsBase>> options, Map<String, String> categoryDescriptions, OptionsParser.HelpVerbosity helpVerbosity, String productName) { + return expandHelpTopic( + topic, + help, + commandClass, + options, + categoryDescriptions, + helpVerbosity, + productName, + false); + } + + public static final String expandHelpTopic( + String topic, + String help, + Class<? extends BlazeCommand> commandClass, + Collection<Class<? extends OptionsBase>> options, + Map<String, String> categoryDescriptions, + OptionsParser.HelpVerbosity helpVerbosity, + String productName, + boolean useNewCategoryEnum) { OptionsParser parser = OptionsParser.newOptionsParser(options); String template; @@ -137,8 +157,12 @@ public class BlazeCommandUtils { try { template = ResourceFileLoader.loadResource(commandClass, resourceName); } catch (IOException e) { - throw new IllegalStateException("failed to load help resource '" + resourceName - + "' due to I/O error: " + e.getMessage(), e); + throw new IllegalStateException( + "failed to load help resource '" + + resourceName + + "' due to I/O error: " + + e.getMessage(), + e); } } else { template = help; @@ -148,10 +172,16 @@ public class BlazeCommandUtils { throw new IllegalStateException("Help template for '" + topic + "' omits %{options}!"); } - String optionStr = - parser - .describeOptions(categoryDescriptions, helpVerbosity) - .replace("%{product}", productName); + String optionStr; + if (useNewCategoryEnum) { + optionStr = + parser.describeOptions(productName, helpVerbosity).replace("%{product}", productName); + } else { + optionStr = + parser + .describeOptionsWithDeprecatedCategories(categoryDescriptions, helpVerbosity) + .replace("%{product}", productName); + } return template .replace("%{product}", productName) .replace("%{command}", topic) @@ -166,10 +196,10 @@ public class BlazeCommandUtils { /** * The help page for this command. * - * @param categoryDescriptions a mapping from option category names to - * descriptions, passed to {@link OptionsParser#describeOptions}. - * @param verbosity a tri-state verbosity option selecting between just names, - * names and syntax, and full description. + * @param categoryDescriptions a mapping from option category names to descriptions, passed to + * {@link OptionsParser#describeOptionsWithDeprecatedCategories}. + * @param verbosity a tri-state verbosity option selecting between just names, names and syntax, + * and full description. */ public static String getUsage( Class<? extends BlazeCommand> commandClass, @@ -177,7 +207,8 @@ public class BlazeCommandUtils { OptionsParser.HelpVerbosity verbosity, Iterable<BlazeModule> blazeModules, ConfiguredRuleClassProvider ruleClassProvider, - String productName) { + String productName, + boolean useNewCategoryEnum) { Command commandAnnotation = commandClass.getAnnotation(Command.class); return BlazeCommandUtils.expandHelpTopic( commandAnnotation.name(), @@ -186,6 +217,7 @@ public class BlazeCommandUtils { BlazeCommandUtils.getOptions(commandClass, blazeModules, ruleClassProvider), categoryDescriptions, verbosity, - productName); + productName, + useNewCategoryEnum); } } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java index 8c3821bff6..8b1d9a8f03 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java @@ -43,6 +43,8 @@ import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDefinition; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionFilterDescriptions; +import com.google.devtools.common.options.OptionMetadataTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsProvider; @@ -113,15 +115,25 @@ public final class HelpCommand implements BlazeCommand { help = "Show only the names of the options, not their types or meanings." ) public Void showShortFormOptions; + + @Option( + name = "use_new_category_enum", + defaultValue = "false", + documentationCategory = OptionDocumentationCategory.LOGGING, + effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT}, + metadataTags = {OptionMetadataTag.EXPERIMENTAL} + ) + public boolean useNewCategoryEnum; } /** - * Returns a map that maps option categories to descriptive help strings for categories that - * are not part of the Bazel core. + * Returns a map that maps option categories to descriptive help strings for categories that are + * not part of the Bazel core. */ - private static ImmutableMap<String, String> getOptionCategories(BlazeRuntime runtime) { + @Deprecated + private static ImmutableMap<String, String> getDeprecatedOptionCategoriesDescriptions( + String name) { ImmutableMap.Builder<String, String> optionCategoriesBuilder = ImmutableMap.builder(); - String name = runtime.getProductName(); optionCategoriesBuilder .put("checking", String.format( "Checking options, which control %s's error checking and/or warnings", name)) @@ -180,27 +192,40 @@ public final class HelpCommand implements BlazeCommand { return ExitCode.COMMAND_LINE_ERROR; } String helpSubject = options.getResidue().get(0); - if (helpSubject.equals("startup_options")) { - emitBlazeVersionInfo(outErr, runtime.getProductName()); - emitStartupOptions( - outErr, helpOptions.helpVerbosity, runtime, getOptionCategories(runtime)); - return ExitCode.SUCCESS; - } else if (helpSubject.equals("target-syntax")) { - emitBlazeVersionInfo(outErr, runtime.getProductName()); - emitTargetSyntaxHelp(outErr, getOptionCategories(runtime), runtime.getProductName()); - return ExitCode.SUCCESS; - } else if (helpSubject.equals("info-keys")) { - emitInfoKeysHelp(env, outErr); - return ExitCode.SUCCESS; - } else if (helpSubject.equals("completion")) { - emitCompletionHelp(runtime, outErr); - return ExitCode.SUCCESS; - } else if (helpSubject.equals("flags-as-proto")) { - emitFlagsAsProtoHelp(runtime, outErr); - return ExitCode.SUCCESS; - } else if (helpSubject.equals("everything-as-html")) { - new HtmlEmitter(runtime).emit(outErr); - return ExitCode.SUCCESS; + String productName = runtime.getProductName(); + // Go through the custom subjects before going through Bazel commands. + switch (helpSubject) { + case "startup_options": + emitBlazeVersionInfo(outErr, runtime.getProductName()); + emitStartupOptions( + outErr, + helpOptions.helpVerbosity, + runtime, + getDeprecatedOptionCategoriesDescriptions(productName), + helpOptions.useNewCategoryEnum); + return ExitCode.SUCCESS; + case "target-syntax": + emitBlazeVersionInfo(outErr, runtime.getProductName()); + emitTargetSyntaxHelp( + outErr, + getDeprecatedOptionCategoriesDescriptions(productName), + productName, + helpOptions.useNewCategoryEnum); + + return ExitCode.SUCCESS; + case "info-keys": + emitInfoKeysHelp(env, outErr); + return ExitCode.SUCCESS; + case "completion": + emitCompletionHelp(runtime, outErr); + return ExitCode.SUCCESS; + case "flags-as-proto": + emitFlagsAsProtoHelp(runtime, outErr); + return ExitCode.SUCCESS; + case "everything-as-html": + new HtmlEmitter(runtime, helpOptions.useNewCategoryEnum).emit(outErr); + return ExitCode.SUCCESS; + default: // fall out } BlazeCommand command = runtime.getCommandMap().get(helpSubject); @@ -218,14 +243,17 @@ public final class HelpCommand implements BlazeCommand { return ExitCode.COMMAND_LINE_ERROR; } } - emitBlazeVersionInfo(outErr, runtime.getProductName()); - outErr.printOut(BlazeCommandUtils.getUsage( - command.getClass(), - getOptionCategories(runtime), - helpOptions.helpVerbosity, - runtime.getBlazeModules(), - runtime.getRuleClassProvider(), - runtime.getProductName())); + emitBlazeVersionInfo(outErr, productName); + outErr.printOut( + BlazeCommandUtils.getUsage( + command.getClass(), + getDeprecatedOptionCategoriesDescriptions(productName), + helpOptions.helpVerbosity, + runtime.getBlazeModules(), + runtime.getRuleClassProvider(), + productName, + helpOptions.useNewCategoryEnum)); + return ExitCode.SUCCESS; } @@ -235,16 +263,22 @@ public final class HelpCommand implements BlazeCommand { outErr.printOut(String.format("%80s\n", line)); } - private void emitStartupOptions(OutErr outErr, OptionsParser.HelpVerbosity helpVerbosity, - BlazeRuntime runtime, ImmutableMap<String, String> optionCategories) { + private void emitStartupOptions( + OutErr outErr, + OptionsParser.HelpVerbosity helpVerbosity, + BlazeRuntime runtime, + ImmutableMap<String, String> optionCategories, + boolean useNewCategoryEnum) { outErr.printOut( - BlazeCommandUtils.expandHelpTopic("startup_options", + BlazeCommandUtils.expandHelpTopic( + "startup_options", "resource:startup_options.txt", getClass(), BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()), optionCategories, helpVerbosity, - runtime.getProductName())); + runtime.getProductName(), + useNewCategoryEnum)); } private void emitCompletionHelp(BlazeRuntime runtime, OutErr outErr) { @@ -345,15 +379,21 @@ public final class HelpCommand implements BlazeCommand { return ImmutableSortedMap.copyOf(runtime.getCommandMap()); } - private void emitTargetSyntaxHelp(OutErr outErr, ImmutableMap<String, String> optionCategories, - String productName) { - outErr.printOut(BlazeCommandUtils.expandHelpTopic("target-syntax", - "resource:target-syntax.txt", - getClass(), - ImmutableList.<Class<? extends OptionsBase>>of(), - optionCategories, - OptionsParser.HelpVerbosity.MEDIUM, - productName)); + private void emitTargetSyntaxHelp( + OutErr outErr, + ImmutableMap<String, String> optionCategories, + String productName, + boolean useNewCategoryEnum) { + outErr.printOut( + BlazeCommandUtils.expandHelpTopic( + "target-syntax", + "resource:target-syntax.txt", + getClass(), + ImmutableList.<Class<? extends OptionsBase>>of(), + optionCategories, + OptionsParser.HelpVerbosity.MEDIUM, + productName, + useNewCategoryEnum)); } private void emitInfoKeysHelp(CommandEnvironment env, OutErr outErr) { @@ -400,11 +440,19 @@ public final class HelpCommand implements BlazeCommand { private static final class HtmlEmitter { private final BlazeRuntime runtime; - private final ImmutableMap<String, String> optionCategories; + private final ImmutableMap<String, String> deprecatedOptionCategoryDescriptions; + private final boolean useNewCategoriesEnum; - private HtmlEmitter(BlazeRuntime runtime) { + private HtmlEmitter(BlazeRuntime runtime, boolean useNewCategoriesEnum) { this.runtime = runtime; - this.optionCategories = getOptionCategories(runtime); + this.useNewCategoriesEnum = useNewCategoriesEnum; + String productName = runtime.getProductName(); + if (useNewCategoriesEnum) { + this.deprecatedOptionCategoryDescriptions = null; + } else { + this.deprecatedOptionCategoryDescriptions = + getDeprecatedOptionCategoriesDescriptions(productName); + } } private void emit(OutErr outErr) { @@ -478,14 +526,67 @@ public final class HelpCommand implements BlazeCommand { result.append("\n"); } } + + // Describe the tags once, any mentions above should link to these descriptions. + if (useNewCategoriesEnum) { + String productName = runtime.getProductName(); + ImmutableMap<OptionEffectTag, String> effectTagDescriptions = + OptionFilterDescriptions.getOptionEffectTagDescription(productName); + result.append("<h3>Option Effect Tags</h3>\n"); + result.append("<table>\n"); + for (OptionEffectTag tag : OptionEffectTag.values()) { + String tagDescription = effectTagDescriptions.get(tag); + + result.append("<tr>\n"); + result.append( + String.format( + "<td id=\"effect_tag_%s\"><code>%s</code></td>\n", + tag, tag.name().toLowerCase())); + result.append(String.format("<td>%s</td>\n", HTML_ESCAPER.escape(tagDescription))); + result.append("</tr>\n"); + } + result.append("</table>\n"); + + ImmutableMap<OptionMetadataTag, String> metadataTagDescriptions = + OptionFilterDescriptions.getOptionMetadataTagDescription(productName); + result.append("<h3>Option Metadata Tags</h3>\n"); + result.append("<table>\n"); + for (OptionMetadataTag tag : OptionMetadataTag.values()) { + // skip the tags that are reserved for undocumented flags. + if (!tag.equals(OptionMetadataTag.HIDDEN) && !tag.equals(OptionMetadataTag.INTERNAL)) { + String tagDescription = metadataTagDescriptions.get(tag); + + result.append("<tr>\n"); + result.append( + String.format( + "<td id=\"metadata_tag_%s\"><code>%s</code></td>\n", + tag, tag.name().toLowerCase())); + result.append(String.format("<td>%s</td>\n", HTML_ESCAPER.escape(tagDescription))); + result.append("</tr>\n"); + } + } + result.append("</table>\n"); + } + outErr.printOut(result.toString()); } private void appendOptionsHtml( StringBuilder result, Iterable<Class<? extends OptionsBase>> optionsClasses) { OptionsParser parser = OptionsParser.newOptionsParser(optionsClasses); - result.append(parser.describeOptionsHtml(optionCategories, HTML_ESCAPER) - .replace("%{product}", runtime.getProductName())); + String productName = runtime.getProductName(); + if (useNewCategoriesEnum) { + result.append( + parser + .describeOptionsHtml(HTML_ESCAPER, productName) + .replace("%{product}", productName)); + } else { + result.append( + parser + .describeOptionsHtmlWithDeprecatedCategories( + deprecatedOptionCategoryDescriptions, HTML_ESCAPER) + .replace("%{product}", productName)); + } } private static String capitalize(String s) { @@ -510,3 +611,4 @@ public final class HelpCommand implements BlazeCommand { void visit(String commandName, Command commandAnnotation, OptionsParser parser); } } + diff --git a/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java new file mode 100644 index 0000000000..4c6efefe1d --- /dev/null +++ b/src/main/java/com/google/devtools/common/options/OptionFilterDescriptions.java @@ -0,0 +1,175 @@ +// 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.common.options; + +import com.google.common.collect.ImmutableMap; + +/** + * Provides descriptions of the options filters, for use in generated documentation and usage text. + */ +public class OptionFilterDescriptions { + + /** The order that the categories should be listed in. */ + static OptionDocumentationCategory[] documentationOrder = { + OptionDocumentationCategory.BAZEL_CLIENT_OPTIONS, + OptionDocumentationCategory.EXECUTION_STRATEGY, + OptionDocumentationCategory.TOOLCHAIN, + OptionDocumentationCategory.OUTPUT_SELECTION, + OptionDocumentationCategory.OUTPUT_PARAMETERS, + OptionDocumentationCategory.SIGNING, + OptionDocumentationCategory.TESTING, + OptionDocumentationCategory.QUERY, + OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION, + OptionDocumentationCategory.LOGGING, + OptionDocumentationCategory.GENERIC_INPUTS, + OptionDocumentationCategory.UNCATEGORIZED + }; + + static ImmutableMap<OptionDocumentationCategory, String> getOptionCategoriesEnumDescription( + String productName) { + ImmutableMap.Builder<OptionDocumentationCategory, String> optionCategoriesBuilder = + ImmutableMap.builder(); + optionCategoriesBuilder + .put( + OptionDocumentationCategory.UNCATEGORIZED, + "Miscellaneous options, not otherwise categorized.") + .put( // Here for completeness, the help output should not include this option. + OptionDocumentationCategory.UNDOCUMENTED, + "This feature should not be documented, as it is not meant for general use") + .put( + OptionDocumentationCategory.BAZEL_CLIENT_OPTIONS, + "Options that appear before the command and are parsed by the client") + .put( + OptionDocumentationCategory.LOGGING, + "Options that affect the verbosity, format or location of logging") + .put(OptionDocumentationCategory.EXECUTION_STRATEGY, "Options that control build execution") + .put( + OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION, + "Options that trigger optimizations of the build time") + .put( + OptionDocumentationCategory.OUTPUT_SELECTION, + "Options that control the output of the command") + .put( + OptionDocumentationCategory.OUTPUT_PARAMETERS, + "Options that let the user configure the intended output, affecting its value, as " + + "opposed to its existence") + .put( + OptionDocumentationCategory.SIGNING, + "Options that affect the signing outputs of a build") + .put( + OptionDocumentationCategory.TESTING, + "Options that govern the behavior of the test environment or test runner") + .put( + OptionDocumentationCategory.TOOLCHAIN, + "Options that configure the toolchain used for action execution") + .put(OptionDocumentationCategory.QUERY, "Options relating to query output and semantics") + .put( + OptionDocumentationCategory.GENERIC_INPUTS, + "Options specifying or altering a generic input to a Bazel command that does not fall " + + "into other categories."); + return optionCategoriesBuilder.build(); + } + + public static ImmutableMap<OptionEffectTag, String> getOptionEffectTagDescription( + String productName) { + ImmutableMap.Builder<OptionEffectTag, String> effectTagDescriptionBuilder = + ImmutableMap.builder(); + effectTagDescriptionBuilder + .put(OptionEffectTag.UNKNOWN, "This option has unknown, or undocumented, effect.") + .put(OptionEffectTag.NO_OP, "This option has literally no effect.") + .put( + OptionEffectTag.LOSES_INCREMENTAL_STATE, + "Changing the value of this option can cause significant loss of incremental " + + "state, which slows builds. State could be lost due to a server restart or to " + + "invalidation of a large part of the dependency graph.") + .put( + OptionEffectTag.CHANGES_INPUTS, + "This option actively changes the inputs that " + + productName + + " considers for the build, such as filesystem restrictions, repository versions, " + + "or other options.") + .put( + OptionEffectTag.AFFECTS_OUTPUTS, + "This option affects " + + productName + + "'s outputs. This tag is intentionally broad, can include transitive affects, " + + "and does not specify the type of output it affects.") + .put( + OptionEffectTag.BUILD_FILE_SEMANTICS, + "This option affects the semantics of BUILD or .bzl files.") + .put( + OptionEffectTag.BAZEL_INTERNAL_CONFIGURATION, + "This option affects settings of " + + productName + + "-internal machinery. This tag does not, on its own, mean that build artifacts " + + "are affected.") + .put( + OptionEffectTag.LOADING_AND_ANALYSIS, + "This option affects the loading and analysis of dependencies, and the building " + + "of the dependency graph.") + .put( + OptionEffectTag.EXECUTION, + "This option affects the execution phase, such as sandboxing or remote execution " + + "related options.") + .put( + OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS, + "This option triggers an optimization that may be machine specific and is not " + + "guaranteed to work on all machines. The optimization could include a tradeoff " + + "with other aspects of performance, such as memory or cpu cost.") + .put( + OptionEffectTag.EAGERNESS_TO_EXIT, + "This option changes how eagerly " + + productName + + " will exit from a failure, where a choice between continuing despite the " + + "failure and ending the invocation exists.") + .put( + OptionEffectTag.BAZEL_MONITORING, + "This option is used to monitor " + productName + "'s behavior and performance.") + .put( + OptionEffectTag.TERMINAL_OUTPUT, + "This option affects " + productName + "'s terminal output.") + .put( + OptionEffectTag.ACTION_OPTIONS, + "This option changes the command line arguments of one or more build actions.") + .put( + OptionEffectTag.TEST_RUNNER, + "This option changes the testrunner environment of the build."); + return effectTagDescriptionBuilder.build(); + } + + public static ImmutableMap<OptionMetadataTag, String> getOptionMetadataTagDescription( + String productName) { + ImmutableMap.Builder<OptionMetadataTag, String> effectTagDescriptionBuilder = + ImmutableMap.builder(); + effectTagDescriptionBuilder + .put( + OptionMetadataTag.EXPERIMENTAL, + "This option triggers an experimental feature with no guarantees of functionality.") + .put( + OptionMetadataTag.INCOMPATIBLE_CHANGE, + "This option triggers a breaking change. Use this option to test your migration " + + "readiness or get early access to the new feature") + .put( + OptionMetadataTag.DEPRECATED, + "This option is deprecated. It might be that the feature it affects is deprecated, " + + "or that another method of supplying the information is preferred.") + .put( + OptionMetadataTag.HIDDEN, // Here for completeness, these options are UNDOCUMENTED. + "This option should not be used by a user, and should not be logged.") + .put( + OptionMetadataTag.INTERNAL, // Here for completeness, these options are UNDOCUMENTED. + "This option isn't even a option, and should not be logged."); + return effectTagDescriptionBuilder.build(); + } +} diff --git a/src/main/java/com/google/devtools/common/options/OptionsParser.java b/src/main/java/com/google/devtools/common/options/OptionsParser.java index 28c2206fbc..2b4bd2a3e4 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsParser.java +++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java @@ -17,8 +17,10 @@ package com.google.devtools.common.options; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; import com.google.common.escape.Escaper; import com.google.devtools.common.options.OptionDefinition.NotAnOptionException; import java.lang.reflect.Constructor; @@ -32,6 +34,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -224,7 +227,9 @@ public class OptionsParser implements OptionsProvider { public void parseAndExitUponError(OptionPriority priority, String source, String[] args) { for (String arg : args) { if (arg.equals("--help")) { - System.out.println(describeOptions(ImmutableMap.of(), HelpVerbosity.LONG)); + System.out.println( + describeOptionsWithDeprecatedCategories(ImmutableMap.of(), HelpVerbosity.LONG)); + System.exit(0); } } @@ -283,6 +288,78 @@ public class OptionsParser implements OptionsProvider { * Returns a description of all the options this parser can digest. In addition to {@link Option} * annotations, this method also interprets {@link OptionsUsage} annotations which give an * intuitive short description for the options. Options of the same category (see {@link + * OptionDocumentationCategory}) will be grouped together. + * + * @param productName the name of this product (blaze, bazel) + * @param helpVerbosity if {@code long}, the options will be described verbosely, including their + * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if + * {@code short}, the options are just enumerated. + */ + public String describeOptions(String productName, HelpVerbosity helpVerbosity) { + StringBuilder desc = new StringBuilder(); + LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory = + getOptionsSortedByCategory(); + ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = + OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); + for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e : + optionsByCategory.entrySet()) { + String categoryDescription = optionCategoryDescriptions.get(e.getKey()); + List<OptionDefinition> categorizedOptionList = e.getValue(); + + // Describe the category if we're going to end up using it at all. + if (!categorizedOptionList.isEmpty()) { + desc.append("\n").append(categoryDescription).append(":\n"); + } + // Describe the options in this category. + for (OptionDefinition optionDef : categorizedOptionList) { + OptionsUsage.getUsage(optionDef, desc, helpVerbosity, impl.getOptionsData(), true); + } + } + + return desc.toString().trim(); + } + + /** + * @return all documented options loaded in this parser, grouped by categories in display order. + */ + private LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> + getOptionsSortedByCategory() { + OptionsData data = impl.getOptionsData(); + if (data.getOptionsClasses().isEmpty()) { + return new LinkedHashMap<>(); + } + + // Get the documented options grouped by category. + ListMultimap<OptionDocumentationCategory, OptionDefinition> optionsByCategories = + ArrayListMultimap.create(); + for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) { + for (OptionDefinition optionDefinition : + OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { + // Only track documented options. + if (optionDefinition.getDocumentationCategory() + != OptionDocumentationCategory.UNDOCUMENTED) { + optionsByCategories.put(optionDefinition.getDocumentationCategory(), optionDefinition); + } + } + } + + // Put the categories into display order and sort the options in each category. + LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> sortedCategoriesToOptions = + new LinkedHashMap<>(OptionFilterDescriptions.documentationOrder.length, 1); + for (OptionDocumentationCategory category : OptionFilterDescriptions.documentationOrder) { + List<OptionDefinition> optionList = optionsByCategories.get(category); + if (optionList != null) { + optionList.sort(OptionDefinition.BY_OPTION_NAME); + sortedCategoriesToOptions.put(category, optionList); + } + } + return sortedCategoriesToOptions; + } + + /** + * Returns a description of all the options this parser can digest. In addition to {@link Option} + * annotations, this method also interprets {@link OptionsUsage} annotations which give an + * intuitive short description for the options. Options of the same category (see {@link * Option#category}) will be grouped together. * * @param categoryDescriptions a mapping from category names to category descriptions. @@ -291,7 +368,8 @@ public class OptionsParser implements OptionsProvider { * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if * {@code short}, the options are just enumerated. */ - public String describeOptions( + @Deprecated + public String describeOptionsWithDeprecatedCategories( Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) { OptionsData data = impl.getOptionsData(); StringBuilder desc = new StringBuilder(); @@ -318,7 +396,8 @@ public class OptionsParser implements OptionsProvider { if (optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { - OptionsUsage.getUsage(optionDefinition, desc, helpVerbosity, impl.getOptionsData()); + OptionsUsage.getUsage( + optionDefinition, desc, helpVerbosity, impl.getOptionsData(), false); } } } @@ -326,17 +405,17 @@ public class OptionsParser implements OptionsProvider { } /** - * Returns a description of all the options this parser can digest. - * In addition to {@link Option} annotations, this method also - * interprets {@link OptionsUsage} annotations which give an intuitive short - * description for the options. + * Returns a description of all the options this parser can digest. In addition to {@link Option} + * annotations, this method also interprets {@link OptionsUsage} annotations which give an + * intuitive short description for the options. * - * @param categoryDescriptions a mapping from category names to category - * descriptions. Options of the same category (see {@link - * Option#category}) will be grouped together, preceded by the description - * of the category. + * @param categoryDescriptions a mapping from category names to category descriptions. Options of + * the same category (see {@link Option#category}) will be grouped together, preceded by the + * description of the category. */ - public String describeOptionsHtml(Map<String, String> categoryDescriptions, Escaper escaper) { + @Deprecated + public String describeOptionsHtmlWithDeprecatedCategories( + Map<String, String> categoryDescriptions, Escaper escaper) { OptionsData data = impl.getOptionsData(); StringBuilder desc = new StringBuilder(); if (!data.getOptionsClasses().isEmpty()) { @@ -366,7 +445,7 @@ public class OptionsParser implements OptionsProvider { if (optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED) { - OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData()); + OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData(), false); } } desc.append("</dl>\n"); @@ -375,6 +454,37 @@ public class OptionsParser implements OptionsProvider { } /** + * Returns a description of all the options this parser can digest. In addition to {@link Option} + * annotations, this method also interprets {@link OptionsUsage} annotations which give an + * intuitive short description for the options. + */ + public String describeOptionsHtml(Escaper escaper, String productName) { + StringBuilder desc = new StringBuilder(); + LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory = + getOptionsSortedByCategory(); + ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = + OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); + + for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e : + optionsByCategory.entrySet()) { + desc.append("<dl>"); + String categoryDescription = optionCategoryDescriptions.get(e.getKey()); + List<OptionDefinition> categorizedOptionsList = e.getValue(); + + // Describe the category if we're going to end up using it at all. + if (!categorizedOptionsList.isEmpty()) { + desc.append(escaper.escape(categoryDescription)).append(":\n"); + } + // Describe the options in this category. + for (OptionDefinition optionDef : categorizedOptionsList) { + OptionsUsage.getUsageHtml(optionDef, desc, escaper, impl.getOptionsData(), true); + } + desc.append("</dl>\n"); + } + return desc.toString(); + } + + /** * Returns a string listing the possible flag completion for this command along with the command * completion if any. See {@link OptionsUsage#getCompletion(OptionDefinition, StringBuilder)} for * more details on the format for the flag completion. diff --git a/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java index 221dcf030f..38248bf940 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java +++ b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java @@ -90,10 +90,9 @@ class OptionsParserImpl { } }; - /** - * Create a new parser object - */ + /** Create a new parser object. Do not accept a null OptionsData object. */ OptionsParserImpl(OptionsData optionsData) { + Preconditions.checkNotNull(optionsData); this.optionsData = optionsData; } diff --git a/src/main/java/com/google/devtools/common/options/OptionsUsage.java b/src/main/java/com/google/devtools/common/options/OptionsUsage.java index 68a460e8e2..ecc7a6b61b 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsUsage.java +++ b/src/main/java/com/google/devtools/common/options/OptionsUsage.java @@ -21,7 +21,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.escape.Escaper; import java.text.BreakIterator; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; /** A renderer for usage messages for any combination of options classes. */ @@ -42,7 +46,7 @@ class OptionsUsage { new ArrayList<>(OptionsData.getAllOptionDefinitionsForClass(optionsClass)); optionDefinitions.sort(OptionDefinition.BY_OPTION_NAME); for (OptionDefinition optionDefinition : optionDefinitions) { - getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data); + getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data, false); } } @@ -95,19 +99,33 @@ class OptionsUsage { } + // Placeholder tag "UNKNOWN" is ignored. + private static boolean shouldEffectTagBeListed(OptionEffectTag effectTag) { + return !effectTag.equals(OptionEffectTag.UNKNOWN); + } + + // Tags that only apply to undocumented options are excluded. + private static boolean shouldMetadataTagBeListed(OptionMetadataTag metadataTag) { + return !metadataTag.equals(OptionMetadataTag.HIDDEN) + && !metadataTag.equals(OptionMetadataTag.INTERNAL); + } + /** Appends the usage message for a single option-field message to 'usage'. */ static void getUsage( OptionDefinition optionDefinition, StringBuilder usage, OptionsParser.HelpVerbosity helpVerbosity, - OptionsData optionsData) { + OptionsData optionsData, + boolean includeTags) { String flagName = getFlagName(optionDefinition); String typeDescription = getTypeDescription(optionDefinition); usage.append(" --").append(flagName); - if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { // just the name + if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { usage.append('\n'); return; } + + // Add the option's type and default information. Stop there for "medium" verbosity. if (optionDefinition.getAbbreviation() != '\0') { usage.append(" [-").append(optionDefinition.getAbbreviation()).append(']'); } @@ -127,9 +145,12 @@ class OptionsUsage { usage.append(")"); } usage.append("\n"); - if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { // just the name and type. + if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { return; } + + // For verbosity "long," add the full description and expansion, along with the tag + // information if requested. if (!optionDefinition.getHelpText().isEmpty()) { usage.append(paragraphFill(optionDefinition.getHelpText(), /*indent=*/ 4, /*width=*/ 80)); usage.append('\n'); @@ -151,9 +172,28 @@ class OptionsUsage { for (String req : optionDefinition.getImplicitRequirements()) { requiredMsg.append(req).append(" "); } - usage.append(paragraphFill(requiredMsg.toString(), /*indent=*/ 6, /*width=*/ 80)); + usage.append(paragraphFill(requiredMsg.toString(), 6, 80)); // (indent, width) usage.append('\n'); } + if (!includeTags) { + return; + } + + // If we are expected to include the tags, add them for high verbosity. + Stream<OptionEffectTag> effectTagStream = + Arrays.stream(optionDefinition.getOptionEffectTags()) + .filter(OptionsUsage::shouldEffectTagBeListed); + Stream<OptionMetadataTag> metadataTagStream = + Arrays.stream(optionDefinition.getOptionMetadataTags()) + .filter(OptionsUsage::shouldMetadataTagBeListed); + String tagList = + Stream.concat(effectTagStream, metadataTagStream) + .map(tag -> tag.toString().toLowerCase()) + .collect(Collectors.joining(", ")); + if (!tagList.isEmpty()) { + usage.append(paragraphFill("Tags: " + tagList, 6, 80)); // (indent, width) + usage.append("\n"); + } } /** Append the usage message for a single option-field message to 'usage'. */ @@ -161,7 +201,8 @@ class OptionsUsage { OptionDefinition optionDefinition, StringBuilder usage, Escaper escaper, - OptionsData optionsData) { + OptionsData optionsData, + boolean includeTags) { String plainFlagName = optionDefinition.getOptionName(); String flagName = getFlagName(optionDefinition); String valueDescription = optionDefinition.getValueTypeHelpText(); @@ -215,7 +256,10 @@ class OptionsUsage { Preconditions.checkArgument(!expansion.isEmpty()); expandsMsg = new StringBuilder("Expands to:<br/>\n"); for (String exp : expansion) { - // TODO(ulfjack): Can we link to the expanded flags here? + // TODO(ulfjack): We should link to the expanded flags, but unfortunately we don't + // currently guarantee that all flags are only printed once. A flag in an OptionBase that + // is included by 2 different commands, but not inherited through a parent command, will + // be printed multiple times. expandsMsg .append(" <code>") .append(escaper.escape(exp)) @@ -225,6 +269,32 @@ class OptionsUsage { usage.append(expandsMsg.toString()); } + // Add effect tags, if not UNKNOWN, and metadata tags, if not empty. + if (includeTags) { + Stream<OptionEffectTag> effectTagStream = + Arrays.stream(optionDefinition.getOptionEffectTags()) + .filter(OptionsUsage::shouldEffectTagBeListed); + Stream<OptionMetadataTag> metadataTagStream = + Arrays.stream(optionDefinition.getOptionMetadataTags()) + .filter(OptionsUsage::shouldMetadataTagBeListed); + String tagList = + Stream.concat( + effectTagStream.map( + tag -> + String.format( + "<a href=\"#effect_tag_%s\"><code>%s</code></a>", + tag, tag.name().toLowerCase())), + metadataTagStream.map( + tag -> + String.format( + "<a href=\"#metadata_tag_%s\"><code>%s</code></a>", + tag, tag.name().toLowerCase()))) + .collect(Collectors.joining(", ")); + if (!tagList.isEmpty()) { + usage.append("<br>Tags: \n").append(tagList); + } + } + usage.append("</dd>\n"); } @@ -263,8 +333,10 @@ class OptionsUsage { builder.append("={auto,yes,no}\n"); builder.append("--no").append(flagName).append("\n"); } else if (fieldType.isEnum()) { - builder.append("={") - .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase()).append("}\n"); + builder + .append("={") + .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase(Locale.ENGLISH)) + .append("}\n"); } else if (fieldType.getSimpleName().equals("Label")) { // String comparison so we don't introduce a dependency to com.google.devtools.build.lib. builder.append("=label\n"); |