Implement field declarations for declared providers.

RELNOTES: Skylark providers can specify allowed fields and their documentation.
PiperOrigin-RevId: 166446104
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
index 5b4363e..3e850d9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
@@ -74,6 +74,7 @@
 import com.google.devtools.build.lib.packages.TargetUtils;
 import com.google.devtools.build.lib.packages.TestSize;
 import com.google.devtools.build.lib.skylarkinterface.Param;
+import com.google.devtools.build.lib.skylarkinterface.ParamType;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
 import com.google.devtools.build.lib.syntax.BaseFunction;
@@ -90,6 +91,7 @@
 import com.google.devtools.build.lib.syntax.SkylarkList;
 import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
 import com.google.devtools.build.lib.syntax.SkylarkSignatureProcessor;
+import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.SkylarkUtils;
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.syntax.Type.ConversionException;
@@ -288,15 +290,53 @@
         defaultValue = "''",
         doc =
             "A description of the provider that can be extracted by documentation generating tools."
+      ),
+      @Param(
+        name = "fields",
+        doc = "If specified, restricts the set of allowed fields. <br>"
+            + "Possible values are:"
+            + "<ul>"
+            + "  <li> list of fields:<br>"
+            + "       <pre class=\"language-python\">provider(fields = ['a', 'b'])</pre><p>"
+            + "  <li> dictionary field name -> documentation:<br>"
+            + "       <pre class=\"language-python\">provider(\n"
+            + "       fields = { 'a' : 'Documentation for a', 'b' : 'Documentation for b' })</pre>"
+            + "</ul>"
+            + "All fields are optional.",
+        allowedTypes = {
+            @ParamType(type = SkylarkList.class, generic1 = String.class),
+            @ParamType(type = SkylarkDict.class)
+        },
+        noneable = true,
+        named = true,
+        positional = false,
+        defaultValue = "None"
       )
     },
     useLocation = true
   )
   private static final BuiltinFunction provider =
       new BuiltinFunction("provider") {
-        public Provider invoke(String doc, Location location) {
+        public Provider invoke(String doc, Object fields, Location location) throws EvalException {
+          Iterable<String> fieldNames = null;
+          if (fields instanceof SkylarkList<?>) {
+            @SuppressWarnings("unchecked")
+            SkylarkList<String> list = (SkylarkList<String>)
+                    SkylarkType.cast(
+                        fields,
+                        SkylarkList.class, String.class, location,
+                        "Expected list of strings or dictionary of string -> string for 'fields'");
+            fieldNames = list;
+          }  else  if (fields instanceof SkylarkDict) {
+            Map<String, String> dict = SkylarkType.castMap(
+                fields,
+                String.class, String.class,
+                "Expected list of strings or dictionary of string -> string for 'fields'");
+            fieldNames = dict.keySet();
+          }
           return new SkylarkProvider(
               "<no name>", // name is set on export.
+              fieldNames,
               location);
         }
       };
@@ -342,7 +382,7 @@
                 + "implicitly added and must not be specified. Attributes "
                 + "<code>visibility</code>, <code>deprecation</code>, <code>tags</code>, "
                 + "<code>testonly</code>, and <code>features</code> are implicitly added and "
-                + "cannot be overriden."
+                + "cannot be overridden."
       ),
       // TODO(bazel-team): need to give the types of these builtin attributes
       @Param(
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkProvider.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkProvider.java
index d76c006..852c6c0 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkProvider.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.packages;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
@@ -46,21 +48,48 @@
    *
    * <p>Needs to be exported later.
    */
-  public SkylarkProvider(String name, Location location) {
-    this(name, SIGNATURE, location);
+  public SkylarkProvider(String name,
+      @Nullable Iterable<String> fields,
+      Location location) {
+    this(name, buildSignature(fields), location);
   }
 
-  public SkylarkProvider(
-      String name, FunctionSignature.WithValues<Object, SkylarkType> signature, Location location) {
+  private SkylarkProvider(
+      String name,
+      FunctionSignature.WithValues<Object, SkylarkType> signature, Location location) {
     super(name, signature, location);
     this.errorMessageFormatForInstances = DEFAULT_ERROR_MESSAFE;
   }
 
+  private static FunctionSignature.WithValues<Object, SkylarkType> buildSignature(
+      @Nullable  Iterable<String> fields) {
+    if (fields == null) {
+      return SIGNATURE;
+    }
+    return
+        FunctionSignature.WithValues.create(
+        FunctionSignature.namedOnly(0, ImmutableList.copyOf(fields).toArray(new String[0]))
+    );
+  }
+
   @Override
   protected Info createInstanceFromSkylark(Object[] args, Location loc) throws EvalException {
-    @SuppressWarnings("unchecked")
-    Map<String, Object> kwargs = (Map<String, Object>) args[0];
-    return new SkylarkInfo(this, kwargs, loc);
+    if (signature.getSignature().getShape().hasKwArg()) {
+      @SuppressWarnings("unchecked")
+      Map<String, Object> kwargs = (Map<String, Object>) args[0];
+      return new SkylarkInfo(this, kwargs, loc);
+    } else {
+      // todo(dslomov): implement shape sharing.
+      ImmutableList<String> names = signature.getSignature().getNames();
+      Preconditions.checkState(names.size() == args.length);
+      ImmutableMap.Builder<String, Object> fields = ImmutableMap.builder();
+      for (int i = 0; i < args.length; i++) {
+        if (args[i] != null) {
+          fields.put(names.get(i), args[i]);
+        }
+      }
+      return new SkylarkInfo(this, fields.build(), loc);
+    }
   }
 
   @Override
@@ -75,8 +104,13 @@
   }
 
   @Override
+  public String getName() {
+    return key != null ? key.getExportedName() : "<no name>";
+  }
+
+  @Override
   public String getPrintableName() {
-    return key != null ? key.getExportedName() : getName();
+    return getName();
   }
 
   @Override
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RequiredProvidersTest.java b/src/test/java/com/google/devtools/build/lib/packages/RequiredProvidersTest.java
index 489303f..0cbea6a 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/RequiredProvidersTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/RequiredProvidersTest.java
@@ -40,7 +40,7 @@
   private static final Provider P_NATIVE = new NativeProvider<Info>(Info.class, "p_native") {};
 
   private static final SkylarkProvider P_SKYLARK =
-      new SkylarkProvider("p_skylark", Location.BUILTIN);
+      new SkylarkProvider("p_skylark", null, Location.BUILTIN);
 
   static {
     try {
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
index 6a7e201..855162d 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
@@ -42,6 +42,7 @@
 import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
 import com.google.devtools.build.lib.packages.SkylarkAspect;
 import com.google.devtools.build.lib.packages.SkylarkAspectClass;
+import com.google.devtools.build.lib.packages.SkylarkInfo;
 import com.google.devtools.build.lib.packages.SkylarkProvider;
 import com.google.devtools.build.lib.packages.SkylarkProviderIdentifier;
 import com.google.devtools.build.lib.skyframe.SkylarkImportLookupFunction;
@@ -1466,6 +1467,89 @@
     assertThat(p1.getKey()).isEqualTo(new SkylarkProvider.SkylarkKey(FAKE_LABEL, "p"));
   }
 
+  @Test
+  public void providerWithFields() throws Exception {
+    evalAndExport(
+        "p = provider(fields = ['x', 'y'])",
+        "p1 = p(x = 1, y = 2)",
+        "x = p1.x",
+        "y = p1.y"
+    );
+    SkylarkProvider p = (SkylarkProvider) lookup("p");
+    SkylarkInfo p1 = (SkylarkInfo) lookup("p1");
+
+
+    assertThat(p1.getProvider()).isEqualTo(p);
+    assertThat(lookup("x")).isEqualTo(1);
+    assertThat(lookup("y")).isEqualTo(2);
+  }
+
+  @Test
+  public void providerWithFieldsDict() throws Exception {
+    evalAndExport(
+        "p = provider(fields = { 'x' : 'I am x', 'y' : 'I am y'})",
+        "p1 = p(x = 1, y = 2)",
+        "x = p1.x",
+        "y = p1.y"
+    );
+    SkylarkProvider p = (SkylarkProvider) lookup("p");
+    SkylarkInfo p1 = (SkylarkInfo) lookup("p1");
+
+
+    assertThat(p1.getProvider()).isEqualTo(p);
+    assertThat(lookup("x")).isEqualTo(1);
+    assertThat(lookup("y")).isEqualTo(2);
+  }
+
+  @Test
+  public void providerWithFieldsOptional() throws Exception {
+    evalAndExport(
+        "p = provider(fields = ['x', 'y'])",
+        "p1 = p(y = 2)",
+        "y = p1.y"
+    );
+    SkylarkProvider p = (SkylarkProvider) lookup("p");
+    SkylarkInfo p1 = (SkylarkInfo) lookup("p1");
+
+
+    assertThat(p1.getProvider()).isEqualTo(p);
+    assertThat(lookup("y")).isEqualTo(2);
+  }
+
+  @Test
+  public void providerWithFieldsOptionalError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        "p = provider(fields = ['x', 'y'])",
+        "p1 = p(y = 2)",
+        "x = p1.x"
+    );
+    MoreAsserts.assertContainsEvent(ev.getEventCollector(),
+        " 'p' object has no attribute 'x'");
+  }
+
+  @Test
+  public void providerWithExtraFieldsError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        "p = provider(fields = ['x', 'y'])",
+        "p1 = p(x = 1, y = 2, z = 3)"
+    );
+    MoreAsserts.assertContainsEvent(ev.getEventCollector(),
+        "unexpected keyword 'z' in call to p(*, x = ?, y = ?)");
+  }
+
+  @Test
+  public void providerWithEmptyFieldsError() throws Exception {
+    ev.setFailFast(false);
+    evalAndExport(
+        "p = provider(fields = [])",
+        "p1 = p(x = 1, y = 2, z = 3)"
+    );
+    MoreAsserts.assertContainsEvent(ev.getEventCollector(),
+        "unexpected keywords 'x', 'y', 'z' in call to p()");
+  }
+
 
   @Test
   public void starTheOnlyAspectArg() throws Exception {