rust_analyzer: don't build a tree of RustAnalyzerInfos (#3028)

This patch adds an `id` to RustAnalyzerInfo, and replace all the
recursive RustAnalyzerInfos with the respective crate `id`s.
This fixes RustAnalyzerInfos becoming exponentially expensive to build
in the presence of aliases.

---

Before this patch, `RustAnalyzerInfo.deps` contains the RustAnalyzerInfo
of the target crate's deps, and so on recursively.
This forms a graph of Infos where there is one Info per target, but
multiple paths from one target to another.
e.g. these could all point to `bar`: `foo.deps[0] == foo.deps[1].deps[0]
== foo.aliases[0]`.

If we walk `foo` as a tree, we will see `bar` multiple times. Two
operations that do this are `print(info)` and `{info: 42}` which hashes
the object. As the graph grows, the number of paths grows
combinatorically.

`RustAnalyzerInfo.aliases` is defined as a dict from `RustAnalyzerInfo
=> string`, so building this triggers the slowdown.

It would be possible to fix this by e.g. replacing this dict with a list
of pairs.
However the work `rust_analyzer_aspect` does is fundamentally local and
does not need a recursive data structure. Eliminating it means we can
freely use these values as keys, print them, etc.

---

Timings for `ra_ap_rust-analyzer` (which heavily aliases its deps):

```
blaze build //third_party/bazel_rules/rules_rust/tools/rust_analyzer:gen_rust_project; \
time blaze run //third_party/bazel_rules/rules_rust/tools/rust_analyzer:gen_rust_project -- //third_party/rust/ra_ap_rust_analyzer/...

===Before===
Executed in  211.06 secs    fish           external
   usr time    0.24 secs    0.00 micros    0.24 secs
   sys time    1.24 secs  604.00 micros    1.24 sec

===After===
Executed in    3.24 secs    fish           external
   usr time    0.15 secs  389.00 micros    0.15 secs
   sys time    1.18 secs  125.00 micros    1.18 secs
```
diff --git a/extensions/prost/private/prost.bzl b/extensions/prost/private/prost.bzl
index b05cf6d..c615a27 100644
--- a/extensions/prost/private/prost.bzl
+++ b/extensions/prost/private/prost.bzl
@@ -261,7 +261,10 @@
     # https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
     cfgs = ["test", "debug_assertions"]
 
+    crate_id = "prost-" + dep_variant_info.crate_info.root.path
+
     rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
+        id = crate_id,
         aliases = {},
         crate = dep_variant_info.crate_info,
         cfgs = cfgs,
@@ -332,7 +335,10 @@
                 transitive = transitive,
             ),
         ),
-        RustAnalyzerGroupInfo(deps = [proto_dep[RustAnalyzerInfo]]),
+        RustAnalyzerGroupInfo(
+            crate_specs = proto_dep[RustAnalyzerInfo].crate_specs,
+            deps = proto_dep[RustAnalyzerInfo].deps,
+        ),
     ]
 
 rust_prost_library = rule(
diff --git a/rust/private/providers.bzl b/rust/private/providers.bzl
index 97c4500..0482cab 100644
--- a/rust/private/providers.bzl
+++ b/rust/private/providers.bzl
@@ -157,13 +157,14 @@
 RustAnalyzerInfo = provider(
     doc = "RustAnalyzerInfo holds rust crate metadata for targets",
     fields = {
-        "aliases": "Dict[RustAnalyzerInfo, String]: Replacement names these targets should be known as in Rust code",
+        "aliases": "Dict[String, String]: Maps crate IDs to Replacement names these targets should be known as in Rust code",
         "build_info": "BuildInfo: build info for this crate if present",
         "cfgs": "List[String]: features or other compilation `--cfg` settings",
         "crate": "CrateInfo: Crate information.",
         "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
-        "deps": "List[RustAnalyzerInfo]: direct dependencies",
+        "deps": "List[String]: IDs of direct dependency crates",
         "env": "Dict[String: String]: Environment variables, used for the `env!` macro",
+        "id": "String: Arbitrary unique ID for this crate",
         "proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
     },
 )
@@ -171,6 +172,7 @@
 RustAnalyzerGroupInfo = provider(
     doc = "RustAnalyzerGroupInfo holds multiple RustAnalyzerInfos",
     fields = {
-        "deps": "List[RustAnalyzerInfo]: direct dependencies",
+        "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
+        "deps": "List[String]: crate IDs of direct dependencies",
     },
 )
diff --git a/rust/private/rust_analyzer.bzl b/rust/private/rust_analyzer.bzl
index bc12306..6673a45 100644
--- a/rust/private/rust_analyzer.bzl
+++ b/rust/private/rust_analyzer.bzl
@@ -53,6 +53,7 @@
         cfgs = base_info.cfgs,
         env = base_info.env,
         deps = base_info.deps,
+        id = base_info.id,
         crate_specs = depset(direct = [crate_spec], transitive = [base_info.crate_specs]),
         proc_macro_dylib_path = base_info.proc_macro_dylib_path,
         build_info = base_info.build_info,
@@ -72,21 +73,6 @@
 
     return rust_analyzer_info
 
-def _accumulate_rust_analyzer_info(dep_infos_to_accumulate, label_index_to_accumulate, dep):
-    if dep == None:
-        return
-    if RustAnalyzerInfo in dep:
-        label_index_to_accumulate[dep.label] = dep[RustAnalyzerInfo]
-        dep_infos_to_accumulate.append(dep[RustAnalyzerInfo])
-    if RustAnalyzerGroupInfo in dep:
-        for expanded_dep in dep[RustAnalyzerGroupInfo].deps:
-            label_index_to_accumulate[expanded_dep.crate.owner] = expanded_dep
-            dep_infos_to_accumulate.append(expanded_dep)
-
-def _accumulate_rust_analyzer_infos(dep_infos_to_accumulate, label_index_to_accumulate, deps_attr):
-    for dep in deps_attr:
-        _accumulate_rust_analyzer_info(dep_infos_to_accumulate, label_index_to_accumulate, dep)
-
 def _rust_analyzer_aspect_impl(target, ctx):
     if (rust_common.crate_info not in target and
         rust_common.test_crate_info not in target and
@@ -107,22 +93,31 @@
         cfgs += [f[6:] for f in ctx.rule.attr.rustc_flags if f.startswith("--cfg ") or f.startswith("--cfg=")]
 
     build_info = None
-    dep_infos = []
-    labels_to_rais = {}
-
     for dep in getattr(ctx.rule.attr, "deps", []):
         # Save BuildInfo if we find any (for build script output)
         if BuildInfo in dep:
             build_info = dep[BuildInfo]
 
-    _accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "deps", []))
-    _accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "proc_macro_deps", []))
+    # Gather required info from dependencies.
+    label_to_id = {}  # {Label of dependency => crate_id}
+    crate_specs = []  # [depset of File - transitive crate_spec.json files]
+    attrs = ctx.rule.attr
+    all_deps = getattr(attrs, "deps", []) + getattr(attrs, "proc_macro_deps", []) + \
+               [dep for dep in [getattr(attrs, "crate", None), getattr(attrs, "actual", None)] if dep != None]
+    for dep in all_deps:
+        if RustAnalyzerInfo in dep:
+            label_to_id[dep.label] = dep[RustAnalyzerInfo].id
+            crate_specs.append(dep[RustAnalyzerInfo].crate_specs)
+        if RustAnalyzerGroupInfo in dep:
+            for expanded_dep in dep[RustAnalyzerGroupInfo].deps:
+                label_to_id[expanded_dep] = expanded_dep
+            crate_specs.append(dep[RustAnalyzerGroupInfo].crate_specs)
 
-    _accumulate_rust_analyzer_info(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "crate", None))
-    _accumulate_rust_analyzer_info(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "actual", None))
+    deps = label_to_id.values()
+    crate_specs = depset(transitive = crate_specs)
 
     if rust_common.crate_group_info in target:
-        return [RustAnalyzerGroupInfo(deps = dep_infos)]
+        return [RustAnalyzerGroupInfo(deps = deps, crate_specs = crate_specs)]
     elif rust_common.crate_info in target:
         crate_info = target[rust_common.crate_info]
     elif rust_common.test_crate_info in target:
@@ -130,18 +125,23 @@
     else:
         fail("Unexpected target type: {}".format(target))
 
-    aliases = {}
-    for aliased_target, aliased_name in getattr(ctx.rule.attr, "aliases", {}).items():
-        if aliased_target.label in labels_to_rais:
-            aliases[labels_to_rais[aliased_target.label]] = aliased_name
+    aliases = {
+        label_to_id[target.label]: name
+        for (target, name) in getattr(attrs, "aliases", {}).items()
+        if target.label in label_to_id
+    }
+
+    # An arbitrary unique and stable identifier.
+    crate_id = "ID-" + crate_info.root.path
 
     rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
+        id = crate_id,
         aliases = aliases,
         crate = crate_info,
         cfgs = cfgs,
         env = crate_info.rustc_env,
-        deps = dep_infos,
-        crate_specs = depset(transitive = [dep.crate_specs for dep in dep_infos]),
+        deps = deps,
+        crate_specs = crate_specs,
         proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
         build_info = build_info,
     ))
@@ -193,14 +193,6 @@
 _EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
 _OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"
 
-def _crate_id(crate_info):
-    """Returns a unique stable identifier for a crate
-
-    Returns:
-        (string): This crate's unique stable id.
-    """
-    return "ID-" + crate_info.root.path
-
 def _create_single_crate(ctx, attrs, info):
     """Creates a crate in the rust-project.json format.
 
@@ -214,8 +206,7 @@
     """
     crate_name = info.crate.name
     crate = dict()
-    crate_id = _crate_id(info.crate)
-    crate["crate_id"] = crate_id
+    crate["crate_id"] = info.id
     crate["display_name"] = crate_name
     crate["edition"] = info.crate.edition
     crate["env"] = {}
@@ -263,8 +254,8 @@
     # There's one exception - if the dependency is the same crate name as the
     # the crate being processed, we don't add it as a dependency to itself. This is
     # common and expected - `rust_test.crate` pointing to the `rust_library`.
-    crate["deps"] = [_crate_id(dep.crate) for dep in info.deps if _crate_id(dep.crate) != crate_id]
-    crate["aliases"] = {_crate_id(alias_target.crate): alias_name for alias_target, alias_name in info.aliases.items()}
+    crate["deps"] = [id for id in info.deps if id != info.id]
+    crate["aliases"] = info.aliases
     crate["cfg"] = info.cfgs
     toolchain = find_toolchain(ctx)
     crate["target"] = (_EXEC_ROOT_TEMPLATE + toolchain.target_json.path) if toolchain.target_json else toolchain.target_flag_value