// Copyright 2018 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.starlarkbuildapi;

import com.google.devtools.build.docgen.annot.DocCategory;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.ParamType;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.NoneType;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.StarlarkCallable;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;

/** Command line args module. */
@StarlarkBuiltin(
    name = "Args",
    category = DocCategory.BUILTIN,
    doc =
        "An object that encapsulates, in a memory-efficient way, the data needed to build part or "
            + "all of a command line."
            + "" //
            + "<p>It often happens that an action requires a large command line containing values"
            + " accumulated from transitive dependencies. For example, a linker command line might"
            + " list every object file needed by all of the libraries being linked. It is best"
            + " practice to store such transitive data in <a"
            + " href='../builtins/depset.html'><code>depset</code></a>s, so that they can be shared"
            + " by multiple targets. However, if the rule author had to convert these depsets into"
            + " lists of strings in order to construct an action command line, it would defeat this"
            + " memory-sharing optimization." //
            + "<p>For this reason, the action-constructing functions accept <code>Args</code> "
            + "objects in addition to strings. Each <code>Args</code> object represents a "
            + "concatenation of strings and depsets, with optional transformations for "
            + "manipulating the data. <code>Args</code> objects do not process the depsets they "
            + "encapsulate until the execution phase, when it comes time to calculate the command "
            + "line. This helps defer any expensive copying until after the analysis phase is "
            + "complete. See the <a href='https://bazel.build/rules/performance'>Optimizing "
            + "Performance</a> page for more information."
            + "" //
            + "<p><code>Args</code> are constructed by calling <a"
            + " href='../builtins/actions.html#args'><code>ctx.actions.args()</code></a>. They can"
            + " be passed as the <code>arguments</code> parameter of <a"
            + " href='../builtins/actions.html#run'><code>ctx.actions.run()</code></a> or <a"
            + " href='../builtins/actions.html#run_shell'><code>ctx.actions.run_shell()</code></a>."
            + " Each mutation of an <code>Args</code> object appends values to the eventual command"
            + " line." //
            + "<p>The <code>map_each</code> feature allows you to customize how items are "
            + "transformed into strings. If you do not provide a <code>map_each</code> function, "
            + "the standard conversion is as follows: "
            + "<ul>" //
            + "<li>Values that are already strings are left as-is." //
            + "<li><a href='../builtins/File.html'><code>File</code></a> objects are turned into"
            + " their     <code>File.path</code> values." //
            + "<li><a href='../builtins/Label.html'><code>Label</code></a> objects are turned into"
            + " a string representation that resolves back to the same object when resolved in the"
            + " context of the main repository. If possible, the string representation uses the"
            + " apparent name of a repository in favor of the repository's canonical name, which"
            + " makes this representation suited for use in BUILD files. While the exact form of"
            + " the representation is not guaranteed, typical examples are"
            + " <code>//foo:bar</code>, <code>@repo//foo:bar</code> and"
            + " <code>@@canonical_name+//foo:bar.bzl</code>."
            + "<li>All other types are turned into strings in an <i>unspecified</i> manner. For "
            + "    this reason, you should avoid passing values that are not of string or "
            + "    <code>File</code> type to <code>add()</code>, and if you pass them to "
            + "    <code>add_all()</code> or <code>add_joined()</code> then you should provide a "
            + "    <code>map_each</code> function."
            + "</ul>" //
            + ""
            + "<p>When using string formatting (<code>format</code>, <code>format_each</code>, and "
            + "<code>format_joined</code> params of the <code>add*()</code> methods), the format "
            + "template is interpreted in the same way as <code>%</code>-substitution on strings, "
            + "except that the template must have exactly one substitution placeholder and it must "
            + "be <code>%s</code>. Literal percents may be escaped as <code>%%</code>. Formatting "
            + "is applied after the value is converted to a string as per the above."
            + "" //
            + "<p>Each of the <code>add*()</code> methods have an alternate form that accepts an "
            + "extra positional parameter, an \"arg name\" string to insert before the rest of the "
            + "arguments. For <code>add_all</code> and <code>add_joined</code> the extra string "
            + "will not be added if the sequence turns out to be empty. "
            + "For instance, the same usage can add either <code>--foo val1 val2 val3 --bar"
            + "</code> or just <code>--bar</code> to the command line, depending on whether the "
            + "given sequence contains <code>val1..val3</code> or is empty."
            + "" //
            + "<p>If the size of the command line can grow longer than the maximum size allowed by "
            + "the system, the arguments can be spilled over into parameter files. See "
            + "<a href='#use_param_file'><code>use_param_file()</code></a> and "
            + "<a href='#set_param_file_format'><code>set_param_file_format()</code></a>."
            + "" //
            + "<p>Example: Suppose we wanted to generate the command line: "
            + "<pre>\n"
            + "--foo foo1.txt foo2.txt ... fooN.txt --bar bar1.txt,bar2.txt,...,barM.txt --baz\n"
            + "</pre>"
            + "We could use the following <code>Args</code> object: "
            + "<pre class=language-python>\n"
            + "# foo_deps and bar_deps are depsets containing\n"
            + "# File objects for the foo and bar .txt files.\n"
            + "args = ctx.actions.args()\n"
            + "args.add_all(\"--foo\", foo_deps)\n"
            + "args.add_joined(\"--bar\", bar_deps, join_with=\",\")\n"
            + "args.add(\"--baz\")\n"
            + "ctx.actions.run(\n"
            + "  ...\n"
            + "  arguments = [args],\n"
            + "  ...\n"
            + ")\n"
            + "</pre>")
public interface CommandLineArgsApi extends StarlarkValue {
  @StarlarkMethod(
      name = "add",
      doc = "Appends an argument to this command line.",
      parameters = {
        @Param(
            name = "arg_name_or_value",
            doc =
                "If two positional parameters are passed this is interpreted as the arg name. "
                    + "The arg name is added before the value without any processing. "
                    + "If only one positional parameter is passed, it is interpreted as "
                    + "<code>value</code> (see below)."),
        @Param(
            name = "value",
            defaultValue = "unbound",
            doc =
                "The object to append. It will be converted to a string using the standard "
                    + "conversion mentioned above. Since there is no <code>map_each</code> "
                    + "parameter for this function, <code>value</code> should be either a "
                    + "string or a <code>File</code>. A list, tuple, depset, or directory "
                    + "<code>File</code> must be passed to <a href='#add_all'><code>add_all()"
                    + "</code> or <a href='#add_joined'><code>add_joined()</code></a> instead of "
                    + "this method."),
        @Param(
            name = "format",
            allowedTypes = {
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc =
                "A format string pattern, to be applied to the stringified version of <code>value"
                    + "</code>.")
      },
      useStarlarkThread = true)
  CommandLineArgsApi addArgument(
      Object argNameOrValue, Object value, Object format, StarlarkThread thread)
      throws EvalException;

  @StarlarkMethod(
      name = "add_all",
      doc =
          "Appends multiple arguments to this command line. The items are processed lazily during "
              + "the execution phase."
              + ""
              + "<p>Most of the processing occurs over a list of arguments to be appended, as per "
              + "the following steps:"
              + "<ol>"
              + "<li>Each directory <code>File</code> item is replaced by all <code>File</code>s "
              + "recursively contained in that directory."
              + "</li>"
              + "<li>If <code>map_each</code> is given, it is applied to each item, and the "
              + "    resulting lists of strings are concatenated to form the initial argument "
              + "    list. Otherwise, the initial argument list is the result of applying the "
              + "    standard conversion to each item."
              + "<li>Each argument in the list is formatted with <code>format_each</code>, if "
              + "    present."
              + "<li>If <code>uniquify</code> is true, duplicate arguments are removed. The first "
              + "    occurrence is the one that remains."
              + "<li>If a <code>before_each</code> string is given, it is inserted as a new "
              + "    argument before each existing argument in the list. This effectively doubles "
              + "    the number of arguments to be appended by this point."
              + "<li>Except in the case that the list is empty and <code>omit_if_empty</code> is "
              + "    true (the default), the arg name and <code>terminate_with</code> are "
              + "    inserted as the first and last arguments, respectively, if they are given."
              + "</ol>"
              + "Note that empty strings are valid arguments that are subject to all these "
              + "processing steps.",
      parameters = {
        @Param(
            name = "arg_name_or_values",
            doc =
                "If two positional parameters are passed this is interpreted as the arg name. "
                    + "The arg name is added before the <code>values</code> as a separate argument "
                    + "without any processing. This arg name will not be added if "
                    + "<code>omit_if_empty</code> is true (the default) and no other items are "
                    + "appended (as happens if <code>values</code> is empty or all of its items "
                    + "are filtered). "
                    + "If only one positional parameter is passed, it is interpreted as "
                    + "<code>values</code> (see below)."),
        @Param(
            name = "values",
            allowedTypes = {
              @ParamType(type = Sequence.class),
              @ParamType(type = Depset.class),
            },
            defaultValue = "unbound",
            doc = "The list, tuple, or depset whose items will be appended."),
        @Param(
            name = "map_each",
            allowedTypes = {
              @ParamType(type = StarlarkCallable.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc =
                "A function that converts each item to zero or more strings, which may be further"
                    + " processed before appending. If this param is not provided, the standard"
                    + " conversion is used.<p>The function is passed either one or two positional"
                    + " arguments: the item to convert, followed by an optional <a"
                    + " href='../builtins/DirectoryExpander.html'><code>DirectoryExpander</code>"
                    + "</a>. The second argument will be passed only if the supplied function is"
                    + " user-defined (not built-in) and declares more than one parameter.<p>The"
                    + " return value's type depends on how many arguments are to be produced for"
                    + " the item:<ul><li>In the common case when each item turns into one string,"
                    + " the function     should return that string.<li>If the item is to be"
                    + " filtered out entirely, the function should return    "
                    + " <code>None</code>.<li>If the item turns into multiple strings, the function"
                    + " returns a list of     those strings.</ul>Returning a single string or"
                    + " <code>None</code> has the same effect as returning a list of length 1 or"
                    + " length 0 respectively. However, it is more efficient and readable to avoid"
                    + " creating a list where it is not needed.<p>Ordinarily, items that are"
                    + " directories are automatically expanded to their contents when"
                    + " <code>expand_directories=True</code> is set. However, this will not expand"
                    + " directories contained inside other values -- for instance, when the items"
                    + " are structs that have directories as fields. In this situation, the"
                    + " <code>DirectoryExpander</code> argument can be applied to manually obtain"
                    + " the files of a given directory.<p>To avoid unintended retention of large"
                    + " analysis-phase data structures into the execution phase, the"
                    + " <code>map_each</code> function must be declared by a top-level"
                    + " <code>def</code> statement; it may not be a nested function closure by"
                    + " default.<p><i>Warning:</i> <a"
                    + " href='../globals/all.html#print'><code>print()</code></a> statements that"
                    + " are executed during the call to <code>map_each</code> will not produce any"
                    + " visible output."),
        @Param(
            name = "format_each",
            allowedTypes = {
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc =
                "An optional format string pattern, applied to each string returned by the "
                    + "<code>map_each</code> function. "
                    + "The format string must have exactly one '%s' placeholder."),
        @Param(
            name = "before_each",
            allowedTypes = {
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc =
                "An optional argument to append before each argument derived from "
                    + "<code>values</code> is appended."),
        @Param(
            name = "omit_if_empty",
            named = true,
            positional = false,
            defaultValue = "True",
            doc =
                "If true, if there are no arguments derived from <code>values</code> to be "
                    + "appended, then all further processing is suppressed and the command line "
                    + "will be unchanged. If false, the arg name and <code>terminate_with</code>, "
                    + "if provided, will still be appended regardless of whether or not there are "
                    + "other arguments."),
        @Param(
            name = "uniquify",
            named = true,
            positional = false,
            defaultValue = "False",
            doc =
                "If true, duplicate arguments that are derived from <code>values</code> will be "
                    + "omitted. Only the first occurrence of each argument will remain. Usually "
                    + "this feature is not needed because depsets already omit duplicates, "
                    + "but it can be useful if <code>map_each</code> emits the same string for "
                    + "multiple items."),
        @Param(
            name = "expand_directories",
            named = true,
            positional = false,
            defaultValue = "True",
            doc =
                "If true, any directories in <code>values</code> will be expanded to a flat list "
                    + "of files. This happens before <code>map_each</code> is applied."),
        @Param(
            name = "terminate_with",
            allowedTypes = {
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc =
                "An optional argument to append after all other arguments. This argument will not "
                    + "be added if <code>omit_if_empty</code> is true (the default) and no other "
                    + "items are appended (as happens if <code>values</code> is empty or all of "
                    + "its items are filtered)."),
        @Param(
            name = "allow_closure",
            named = true,
            positional = false,
            defaultValue = "False",
            doc =
                "If true, allows the use of closures in function parameters like "
                    + "<code>map_each</code>. Usually this isn't necessary and it risks retaining "
                    + "large analysis-phase data structures into the execution phase."),
      },
      useStarlarkThread = true)
  CommandLineArgsApi addAll(
      Object argNameOrValue,
      Object values,
      Object mapEach,
      Object formatEach,
      Object beforeEach,
      Boolean omitIfEmpty,
      Boolean uniquify,
      Boolean expandDirectories,
      Object terminateWith,
      Boolean allowClosure,
      StarlarkThread thread)
      throws EvalException;

  @StarlarkMethod(
      name = "add_joined",
      doc =
          "Appends an argument to this command line by concatenating together multiple values "
              + "using a separator. The items are processed lazily during the execution phase."
              + ""
              + "<p>Processing is similar to <a href='#add_all'><code>add_all()</code></a>, but "
              + "the list of arguments derived from <code>values</code> is combined into a single "
              + "argument as if by <code>join_with.join(...)</code>, and then formatted using the "
              + "given <code>format_joined</code> string template. Unlike <code>add_all()</code>, "
              + "there is no <code>before_each</code> or <code>terminate_with</code> parameter "
              + "since these are not generally useful when the items are combined into a single "
              + "argument."
              + ""
              + "<p>If after filtering there are no strings to join into an argument, and if "
              + "<code>omit_if_empty</code> is true (the default), no processing is done. "
              + "Otherwise if there are no strings to join but <code>omit_if_empty</code> is "
              + "false, the joined string will be an empty string.",
      parameters = {
        @Param(
            name = "arg_name_or_values",
            doc =
                "If two positional parameters are passed this is interpreted as the arg name. "
                    + "The arg name is added before <code>values</code> without any processing. "
                    + "This arg will not be added if <code>omit_if_empty</code> is true "
                    + "(the default) and there are no strings derived from <code>values</code> "
                    + "to join together (which can happen if <code>values</code> is empty "
                    + "or all of its items are filtered). "
                    + "If only one positional parameter is passed, it is interpreted as "
                    + "<code>values</code> (see below)."),
        @Param(
            name = "values",
            allowedTypes = {
              @ParamType(type = Sequence.class),
              @ParamType(type = Depset.class),
            },
            defaultValue = "unbound",
            doc = "The list, tuple, or depset whose items will be joined."),
        @Param(
            name = "join_with",
            named = true,
            positional = false,
            doc =
                "A delimiter string used to join together the strings obtained from applying "
                    + "<code>map_each</code> and <code>format_each</code>, in the same manner as "
                    + "<a href='../core/string.html#join'><code>string.join()</code></a>."),
        @Param(
            name = "map_each",
            allowedTypes = {
              @ParamType(type = StarlarkCallable.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc = "Same as for <a href='#add_all.map_each'><code>add_all</code></a>."),
        @Param(
            name = "format_each",
            allowedTypes = {
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc = "Same as for <a href='#add_all.format_each'><code>add_all</code></a>."),
        @Param(
            name = "format_joined",
            allowedTypes = {
              @ParamType(type = String.class),
              @ParamType(type = NoneType.class),
            },
            named = true,
            positional = false,
            defaultValue = "None",
            doc =
                "An optional format string pattern applied to the joined string. "
                    + "The format string must have exactly one '%s' placeholder."),
        @Param(
            name = "omit_if_empty",
            named = true,
            positional = false,
            defaultValue = "True",
            doc =
                "If true, if there are no strings to join together (either because <code>values"
                    + "</code> is empty or all its items are filtered), then all further "
                    + "processing is suppressed and the command line will be unchanged. "
                    + "If false, then even if there are no strings to join together, two "
                    + "arguments will be appended: the arg name followed by an empty "
                    + "string (which is the logical join of zero strings)."),
        @Param(
            name = "uniquify",
            named = true,
            positional = false,
            defaultValue = "False",
            doc = "Same as for <a href='#add_all.uniquify'><code>add_all</code></a>."),
        @Param(
            name = "expand_directories",
            named = true,
            positional = false,
            defaultValue = "True",
            doc = "Same as for <a href='#add_all.expand_directories'><code>add_all</code></a>."),
        @Param(
            name = "allow_closure",
            named = true,
            positional = false,
            defaultValue = "False",
            doc = "Same as for <a href='#add_all.allow_closure'><code>add_all</code></a>."),
      },
      useStarlarkThread = true)
  CommandLineArgsApi addJoined(
      Object argNameOrValue,
      Object values,
      String joinWith,
      Object mapEach,
      Object formatEach,
      Object formatJoined,
      Boolean omitIfEmpty,
      Boolean uniquify,
      Boolean expandDirectories,
      Boolean allowClosure,
      StarlarkThread thread)
      throws EvalException;

  @StarlarkMethod(
      name = "use_param_file",
      doc =
          "Spills the args to a params file, replacing them with a pointer to the param file. "
              + "Use when your args may be too large for the system's command length limits."
              + "<p>Bazel may choose to elide writing the params file to the output tree during "
              + "execution for efficiency. "
              + "If you are debugging actions and want to inspect the param file, "
              + "pass <code>--materialize_param_files</code> to your build.",
      parameters = {
        @Param(
            name = "param_file_arg",
            named = true,
            doc =
                "A format string with a single \"%s\". "
                    + "If the args are spilled to a params file then they are replaced "
                    + "with an argument consisting of this string formatted with "
                    + "the path of the params file."
                    + "<p>For example, if the args are spilled to a params file \"params.txt\", "
                    + "then specifying \"--file=%s\" would cause the action command line to "
                    + "contain \"--file=params.txt\"."),
        @Param(
            name = "use_always",
            named = true,
            positional = false,
            defaultValue = "False",
            doc =
                "Whether to always spill the args to a params file. If false, "
                    + "bazel will decide whether the arguments need to be spilled "
                    + "based on your system and arg length.")
      })
  CommandLineArgsApi useParamsFile(String paramFileArg, Boolean useAlways) throws EvalException;

  @StarlarkMethod(
      name = "set_param_file_format",
      doc = "Sets the format of the param file, if one is used",
      parameters = {
        @Param(
            name = "format",
            named = true,
            doc =
                "Must be one of:<ul>"
                    + "<li>\"multiline\": Each item (argument name or value) is"
                    + " written verbatim to the param file with a newline character following"
                    + " it.</li>"
                    + "<li>\"shell\": Same as \"multiline\", but the items are shell-quoted</li>"
                    + "<li>\"flag_per_line\": Same as \"multiline\", but (1) only flags (beginning"
                    + " with '--') are written to the param file, and (2) the values of the flags,"
                    + " if any, are written on the same line with a '=' separator. This is the"
                    + " format expected by the Abseil flags library.</li>"
                    + "</ul><p>The format defaults to \"shell\" if not called.")
      })
  CommandLineArgsApi setParamFileFormat(String format) throws EvalException;

  @StarlarkMethod(
      name = "set_param_file_path",
      doc =
          "Sets the name of the parameter file when used in an action. "
              + "By default, Bazel chooses a path derived from the primary output of the action.",
      parameters = {
        @Param(
            name = "path",
            allowedTypes = {
              @ParamType(type = String.class),
            },
            doc = "The name for the parameter file.")
      })
  CommandLineArgsApi setParamFileName(String path) throws EvalException;
}
