Handle **kwargs and *args macro parameters correctly in Stardoc

Progress toward https://github.com/bazelbuild/skydoc/issues/158

RELNOTES: None.
PiperOrigin-RevId: 235574880
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java
index 62866ef..81dafe9 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java
@@ -24,11 +24,25 @@
   private final String name;
   private final String docString;
   @Nullable private final Object defaultValue;
+  private final boolean mandatory;
 
-  public FunctionParamInfo(String name, String docString, @Nullable Object defaultValue) {
+  private FunctionParamInfo(
+      String name, String docString, @Nullable Object defaultValue, boolean mandatory) {
     this.name = name;
     this.docString = docString;
     this.defaultValue = defaultValue;
+    this.mandatory = mandatory;
+  }
+
+  /** Constructor to be used for normal parameters. */
+  public static FunctionParamInfo forParam(
+      String name, String docString, @Nullable Object defaultValue) {
+    return new FunctionParamInfo(name, docString, defaultValue, defaultValue == null);
+  }
+
+  /** Constructor to be used for *args or **kwargs. */
+  public static FunctionParamInfo forSpecialParam(String name, String docString) {
+    return new FunctionParamInfo(name, docString, null, false);
   }
 
   /**
@@ -74,6 +88,6 @@
    * Returns 'required' if this parameter is mandatory, otherwise returns 'optional'.
    */
   public String getMandatoryString() {
-    return defaultValue == null ? "required" : "optional";
+    return mandatory ? "required" : "optional";
   }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java
index 258f2e8..afe3f3e 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java
@@ -116,28 +116,57 @@
     ImmutableList.Builder<FunctionParamInfo> parameterInfos = ImmutableList.builder();
 
     List<String> paramNames = signature.getSignature().getNames();
+    int numMandatoryParams = signature.getSignature().getShape().getMandatoryPositionals();
+
+    int paramIndex;
+    // Mandatory parameters.
     // Mandatory parameters must always come before optional parameters, so this counts
     // down until all mandatory parameters have been exhausted, and then starts filling in
-    // the default parameters accordingly.
-    int numMandatoryParamsLeft =
-        signature.getDefaultValues() != null
-            ? paramNames.size() - signature.getDefaultValues().size()
-            : paramNames.size();
-    int optionalParamIndex = 0;
+    // the optional parameters accordingly.
+    for (paramIndex = 0; paramIndex < numMandatoryParams; paramIndex++) {
+      String paramName = paramNames.get(paramIndex);
+      String paramDoc = paramNameToDocMap.getOrDefault(paramName, "");
+      parameterInfos.add(FunctionParamInfo.forParam(paramName, paramDoc, /*default param*/ null));
+    }
 
-    for (String paramName : paramNames) {
-      Object defaultParamValue = null;
-      String paramDoc = "";
-      if (numMandatoryParamsLeft == 0) {
-        defaultParamValue = signature.getDefaultValues().get(optionalParamIndex);
-        optionalParamIndex++;
-      } else {
-        numMandatoryParamsLeft--;
+    // Parameters with defaults.
+    if (signature.getDefaultValues() != null) {
+      for (Object element : signature.getDefaultValues()) {
+        String paramName = paramNames.get(paramIndex);
+        String paramDoc = "";
+        Object defaultParamValue = element;
+        if (paramNameToDocMap.containsKey(paramName)) {
+          paramDoc = paramNameToDocMap.get(paramName);
+        }
+        parameterInfos.add(FunctionParamInfo.forParam(paramName, paramDoc, defaultParamValue));
+        paramIndex++;
       }
+    }
+
+    // *arg
+    if (signature.getSignature().getShape().hasStarArg()) {
+      String paramName = paramNames.get(paramIndex);
+      String paramDoc = "";
       if (paramNameToDocMap.containsKey(paramName)) {
         paramDoc = paramNameToDocMap.get(paramName);
+      } else if (paramNameToDocMap.containsKey("*" + paramName)) {
+        paramDoc = paramNameToDocMap.get("*" + paramName);
       }
-      parameterInfos.add(new FunctionParamInfo(paramName, paramDoc, defaultParamValue));
+      parameterInfos.add(FunctionParamInfo.forSpecialParam(paramName, paramDoc));
+      paramIndex++;
+    }
+
+    // **kwargs
+    if (signature.getSignature().getShape().hasKwArg()) {
+      String paramName = paramNames.get(paramIndex);
+      String paramDoc = "";
+      if (paramNameToDocMap.containsKey(paramName)) {
+        paramDoc = paramNameToDocMap.get(paramName);
+      } else if (paramNameToDocMap.containsKey("**" + paramName)) {
+        paramDoc = paramNameToDocMap.get("**" + paramName);
+      }
+      parameterInfos.add(FunctionParamInfo.forSpecialParam(paramName, paramDoc));
+      paramIndex++;
     }
     return parameterInfos.build();
   }
diff --git a/src/test/java/com/google/devtools/build/skydoc/BUILD b/src/test/java/com/google/devtools/build/skydoc/BUILD
index 56a28a2..fcae8f5 100644
--- a/src/test/java/com/google/devtools/build/skydoc/BUILD
+++ b/src/test/java/com/google/devtools/build/skydoc/BUILD
@@ -171,3 +171,10 @@
         "my_module",
     ],
 )
+
+skydoc_test(
+    name = "macro_kwargs_test",
+    golden_file = "testdata/macro_kwargs_test/golden.txt",
+    input_file = "testdata/macro_kwargs_test/input.bzl",
+    skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+)
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/macro_kwargs_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/macro_kwargs_test/golden.txt
new file mode 100644
index 0000000..4c5836a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/macro_kwargs_test/golden.txt
@@ -0,0 +1,160 @@
+## macro_with_args
+
+<pre>
+macro_with_args(<a href="#macro_with_args-name">name</a>, <a href="#macro_with_args-args">args</a>)
+</pre>
+
+My args macro is OK.
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="macro_with_args-name">
+      <td><code>name</code></td>
+      <td>
+        required.
+        <p>
+          The name of the test rule.
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_args-args">
+      <td><code>args</code></td>
+      <td>
+        optional.
+        <p>
+          Other arguments to include
+        </p>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+## macro_with_both
+
+<pre>
+macro_with_both(<a href="#macro_with_both-name">name</a>, <a href="#macro_with_both-number">number</a>, <a href="#macro_with_both-args">args</a>, <a href="#macro_with_both-kwargs">kwargs</a>)
+</pre>
+
+Oh wow this macro has both.
+
+Not much else to say.
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="macro_with_both-name">
+      <td><code>name</code></td>
+      <td>
+        required.
+        <p>
+          The name of the test rule.
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_both-number">
+      <td><code>number</code></td>
+      <td>
+        optional. default is <code>3</code>
+        <p>
+          Some number used for important things
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_both-args">
+      <td><code>args</code></td>
+      <td>
+        optional.
+        <p>
+          Other arguments to include
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_both-kwargs">
+      <td><code>kwargs</code></td>
+      <td>
+        optional.
+        <p>
+          Other attributes to include
+        </p>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+## macro_with_kwargs
+
+<pre>
+macro_with_kwargs(<a href="#macro_with_kwargs-name">name</a>, <a href="#macro_with_kwargs-config">config</a>, <a href="#macro_with_kwargs-deps">deps</a>, <a href="#macro_with_kwargs-kwargs">kwargs</a>)
+</pre>
+
+My kwargs macro is the best.
+
+This is a long multi-line doc string.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
+elementum, diam vitae tincidunt pulvinar, nunc tortor volutpat dui,
+vitae facilisis odio ligula a tortor. Donec ullamcorper odio eget ipsum tincidunt,
+vel mollis eros pellentesque.
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="macro_with_kwargs-name">
+      <td><code>name</code></td>
+      <td>
+        required.
+        <p>
+          The name of the test rule.
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_kwargs-config">
+      <td><code>config</code></td>
+      <td>
+        required.
+        <p>
+          Config to use for my macro
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_kwargs-deps">
+      <td><code>deps</code></td>
+      <td>
+        optional. default is <code>[]</code>
+        <p>
+          List of my macro's dependencies
+        </p>
+      </td>
+    </tr>
+    <tr id="macro_with_kwargs-kwargs">
+      <td><code>kwargs</code></td>
+      <td>
+        optional.
+        <p>
+          Other attributes to include
+        </p>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/macro_kwargs_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/macro_kwargs_test/input.bzl
new file mode 100644
index 0000000..0f6268d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/macro_kwargs_test/input.bzl
@@ -0,0 +1,52 @@
+"""Tests for functions which use *args or **kwargs"""
+
+def macro_with_kwargs(name, config, deps = [], **kwargs):
+    """My kwargs macro is the best.
+
+    This is a long multi-line doc string.
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
+    elementum, diam vitae tincidunt pulvinar, nunc tortor volutpat dui,
+    vitae facilisis odio ligula a tortor. Donec ullamcorper odio eget ipsum tincidunt,
+    vel mollis eros pellentesque.
+
+    Args:
+      name: The name of the test rule.
+      config: Config to use for my macro
+      deps: List of my macro's dependencies
+      **kwargs: Other attributes to include
+
+    Returns:
+      An empty list.
+    """
+    _ignore = [name, config, deps, kwargs]
+    return []
+
+def macro_with_args(name, *args):
+    """My args macro is OK.
+
+    Args:
+      name: The name of the test rule.
+      *args: Other arguments to include
+
+    Returns:
+      An empty list.
+    """
+    _ignore = [name, args]
+    return []
+
+def macro_with_both(name, number = 3, *args, **kwargs):
+    """Oh wow this macro has both.
+
+    Not much else to say.
+
+    Args:
+      name: The name of the test rule.
+      number: Some number used for important things
+      *args: Other arguments to include
+      **kwargs: Other attributes to include
+
+    Returns:
+      An empty list.
+    """
+    _ignore = [name, number, args, kwargs]
+    return []