Implement proto_common.declare_generated_files.

Design doc: https://docs.google.com/document/d/1dY_jfRvnH8SjRXGIfg8av-vquyWsvIZydXJOywvaR1A/edit

PiperOrigin-RevId: 441097041
diff --git a/src/main/starlark/builtins_bzl/common/proto/proto_common.bzl b/src/main/starlark/builtins_bzl/common/proto/proto_common.bzl
index c3625b3..d8c0718 100644
--- a/src/main/starlark/builtins_bzl/common/proto/proto_common.bzl
+++ b/src/main/starlark/builtins_bzl/common/proto/proto_common.bzl
@@ -238,12 +238,57 @@
 
     return bool(included)
 
+def _declare_generated_files(
+        actions,
+        proto_library_target,
+        extension,
+        name_mapper = None):
+    """Declares generated files with a specific extension.
+
+    Use this in lang_proto_library-es when protocol compiler generates files
+    that correspond to .proto file names.
+
+    The function removes ".proto" extension with given one (e.g. ".pb.cc") and
+    declares new output files.
+
+    Args:
+      actions:
+        (ActionFactory) Obtained by ctx.actions, used to declare the files.
+      proto_library_target:
+        (Target) The proto_library to generate the files for.
+        Obtained as the `target` parameter from an aspect's implementation.
+      extension: (str) The extension to use for generated files.
+      name_mapper: (str->str) A function mapped over the base filename without
+        the extension. Used it to replace characters in the name that
+        cause problems in a specific programming language.
+
+    Returns:
+      (list[File]) The list of declared files.
+    """
+    proto_info = proto_library_target[_builtins.toplevel.ProtoInfo]
+    proto_sources = proto_info.direct_sources
+    outputs = []
+
+    for src in proto_sources:
+        basename_no_ext = src.basename[:-(len(src.extension) + 1)]
+
+        if name_mapper:
+            basename_no_ext = name_mapper(basename_no_ext)
+
+        # Note that two proto_library rules can have the same source file, so this is actually a
+        # shared action. NB: This can probably result in action conflicts if the proto_library rules
+        # are not the same.
+        outputs.append(actions.declare_file(basename_no_ext + extension, sibling = src))
+
+    return outputs
+
 proto_common = struct(
     create_proto_compile_action = _create_proto_compile_action,
 )
 
 proto_common_do_not_use = struct(
     compile = _compile,
+    declare_generated_files = _declare_generated_files,
     experimental_should_generate_code = _experimental_should_generate_code,
     experimental_filter_sources = _experimental_filter_sources,
     ProtoLangToolchainInfo = _builtins.internal.ProtoLangToolchainInfo,
diff --git a/src/test/java/com/google/devtools/build/lib/rules/proto/BUILD b/src/test/java/com/google/devtools/build/lib/rules/proto/BUILD
index 6ad4c0f..ec54d98 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/proto/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/proto/BUILD
@@ -19,6 +19,7 @@
         "//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity",
         "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster",
         "//src/main/java/com/google/devtools/build/lib/analysis:configured_target",
+        "//src/main/java/com/google/devtools/build/lib/analysis:file_provider",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/util:os",
diff --git a/src/test/java/com/google/devtools/build/lib/rules/proto/BazelProtoCommonTest.java b/src/test/java/com/google/devtools/build/lib/rules/proto/BazelProtoCommonTest.java
index e853d39..0c2b5c1 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/proto/BazelProtoCommonTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/proto/BazelProtoCommonTest.java
@@ -20,6 +20,7 @@
 import com.google.common.truth.Correspondence;
 import com.google.devtools.build.lib.actions.ResourceSet;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -141,6 +142,24 @@
         "     'proto_dep': attr.label(),",
         "     'toolchain': attr.label(default = '//foo:toolchain'),",
         "  })");
+
+    scratch.file(
+        "foo/declare_generated_files.bzl",
+        "def _impl(ctx):",
+        "  files = proto_common_do_not_use.declare_generated_files(",
+        "    ctx.actions,",
+        "    ctx.attr.proto_dep,",
+        "    ctx.attr.extension,",
+        "    (lambda s: s.replace('-','_').replace('.','/')) if ctx.attr.python_names else None)",
+        "  for f in files:",
+        "    ctx.actions.write(f, '')",
+        "  return [DefaultInfo(files = depset(files))]",
+        "declare_generated_files = rule(_impl,",
+        "  attrs = {",
+        "     'proto_dep': attr.label(),",
+        "     'extension': attr.string(),",
+        "     'python_names': attr.bool(default = False),",
+        "  })");
   }
 
   /** Verifies basic usage of <code>proto_common.generate_code</code>. */
@@ -566,4 +585,37 @@
             + " (third_party/x/something.proto).\n"
             + "Separate '//third_party/x:mixed' into 2 proto_library rules.");
   }
+
+  /** Verifies <code>proto_common.declare_generated_files</code> call. */
+  @Test
+  public void declareGenerateFiles_basic() throws Exception {
+    scratch.file(
+        "bar/BUILD",
+        TestConstants.LOAD_PROTO_LIBRARY,
+        "load('//foo:declare_generated_files.bzl', 'declare_generated_files')",
+        "proto_library(name = 'proto', srcs = ['A.proto', 'b/B.proto'])",
+        "declare_generated_files(name = 'simple', proto_dep = ':proto', extension = '.cc')");
+
+    ConfiguredTarget target = getConfiguredTarget("//bar:simple");
+
+    assertThat(prettyArtifactNames(target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("bar/A.cc", "bar/b/B.cc");
+  }
+
+  /** Verifies <code>proto_common.declare_generated_files</code> call for Python. */
+  @Test
+  public void declareGenerateFiles_pythonc() throws Exception {
+    scratch.file(
+        "bar/BUILD",
+        TestConstants.LOAD_PROTO_LIBRARY,
+        "load('//foo:declare_generated_files.bzl', 'declare_generated_files')",
+        "proto_library(name = 'proto', srcs = ['my-proto.gen.proto'])",
+        "declare_generated_files(name = 'simple', proto_dep = ':proto', extension = '_pb2.py',",
+        "  python_names = True)");
+
+    ConfiguredTarget target = getConfiguredTarget("//bar:simple");
+
+    assertThat(prettyArtifactNames(target.getProvider(FileProvider.class).getFilesToBuild()))
+        .containsExactly("bar/my_proto/gen_pb2.py");
+  }
 }