rust_analyzer_discover/
query.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Context;
4use camino::{Utf8Path, Utf8PathBuf};
5
6use crate::{bazel_command, deserialize_file_content};
7
8#[derive(Clone, Debug, serde_derive::Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct CrateSpec {
11    pub aliases: BTreeMap<String, String>,
12    pub crate_id: String,
13    pub display_name: String,
14    pub edition: String,
15    pub root_module: String,
16    pub is_workspace_member: bool,
17    pub deps: BTreeSet<String>,
18    pub proc_macro_dylib_path: Option<String>,
19    pub source: Option<CrateSpecSource>,
20    pub cfg: BTreeSet<String>,
21    pub env: BTreeMap<String, String>,
22    pub target: String,
23    pub crate_type: CrateType,
24    pub is_test: bool,
25    pub build: Option<CrateSpecBuild>,
26    pub info: Option<InfoFile>,
27}
28
29#[derive(Clone, Debug, serde_derive::Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct CrateSpecBuild {
32    pub label: String,
33    pub build_file: String,
34}
35
36#[derive(Clone, Debug, serde_derive::Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct CrateSpecSource {
39    pub exclude_dirs: Vec<String>,
40    pub include_dirs: Vec<String>,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde_derive::Deserialize)]
44#[serde(rename_all = "kebab-case")]
45pub enum CrateType {
46    Bin,
47    Rlib,
48    Lib,
49    Dylib,
50    Cdylib,
51    Staticlib,
52    ProcMacro,
53}
54
55#[derive(serde_derive::Deserialize)]
56struct CQueryOutput {
57    specs: Vec<Utf8PathBuf>,
58    info: Utf8PathBuf,
59}
60
61#[derive(Clone, Debug, serde_derive::Deserialize)]
62pub struct InfoFile {
63    pub id: String,
64    pub crate_label: Option<String>,
65    pub test_label: Option<String>,
66    pub doc_test_label: Option<String>,
67    pub clippy_label: Option<String>,
68}
69
70#[allow(clippy::too_many_arguments)]
71pub fn get_crate_specs(
72    bazel: &Utf8Path,
73    output_base: &Utf8Path,
74    workspace: &Utf8Path,
75    execution_root: &Utf8Path,
76    bazel_startup_options: &[String],
77    bazel_args: &[String],
78    targets: &[String],
79) -> anyhow::Result<impl IntoIterator<Item = CrateSpec>> {
80    log::info!("running bazel query...");
81    log::debug!("Get crate specs with targets: {targets:?}");
82
83    let query_output = bazel_command(bazel, Some(workspace), Some(output_base))
84        .args(bazel_startup_options)
85        .arg("query")
86        .arg(format!(r#"kind("rust_analyzer_info rule", set({}))"#, targets.join(" ")))
87        .output()
88        .context("bazel query")?;
89
90    if !query_output.status.success() {
91        anyhow::bail!("failed to run bazel query: {}", String::from_utf8_lossy(&query_output.stderr))
92    }
93
94    let stdout = String::from_utf8_lossy(&query_output.stdout);
95    let queried_targets: Vec<_> = stdout.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
96
97    let mut command = bazel_command(bazel, Some(workspace), Some(output_base));
98    command
99        .args(bazel_startup_options)
100        .arg("cquery")
101        .args(bazel_args)
102        .arg(format!("set({})", queried_targets.join(" ")))
103        .arg("--output=starlark")
104        .arg(r#"--starlark:expr={"specs":[file.path for file in target.output_groups.rust_analyzer_crate_spec.to_list()],"info":target.output_groups.rust_analyzer_info.to_list()[0].path}"#)
105        .arg("--build")
106        .arg("--output_groups=rust_analyzer_info,rust_analyzer_proc_macro_dylib,rust_analyzer_src,rust_analyzer_crate_spec");
107
108    log::trace!("Running cquery: {command:#?}");
109    let cquery_output = command.output().context("Failed to spawn cquery command")?;
110
111    log::info!("bazel cquery finished; parsing spec files...");
112
113    let mut outputs = Vec::new();
114    for line in String::from_utf8_lossy(&cquery_output.stdout)
115        .lines()
116        .map(|l| l.trim())
117        .filter(|l| !l.is_empty())
118    {
119        outputs.push(serde_json::from_str::<CQueryOutput>(line).context("parse line")?);
120    }
121
122    let spec_files: BTreeSet<_> = outputs.iter().flat_map(|out| out.specs.iter()).collect();
123    let info_files = outputs
124        .iter()
125        .map(|out| &out.info)
126        .map(|file| {
127            deserialize_file_content::<InfoFile>(&execution_root.join(file), output_base, workspace, execution_root)
128                .map(|file| (file.id.clone(), file))
129        })
130        .collect::<anyhow::Result<BTreeMap<_, _>>>()
131        .context("info file parse")?;
132
133    let crate_specs = spec_files
134        .into_iter()
135        .map(|file| deserialize_file_content(&execution_root.join(file), output_base, workspace, execution_root))
136        .collect::<anyhow::Result<Vec<_>>>()?;
137
138    consolidate_crate_specs(crate_specs, info_files)
139}
140
141/// Read all crate specs, deduplicating crates with the same ID. This happens when
142/// a rust_test depends on a rust_library, for example.
143fn consolidate_crate_specs(
144    crate_specs: Vec<CrateSpec>,
145    mut infos: BTreeMap<String, InfoFile>,
146) -> anyhow::Result<impl IntoIterator<Item = CrateSpec>> {
147    let mut consolidated_specs: BTreeMap<String, CrateSpec> = BTreeMap::new();
148    for mut spec in crate_specs.into_iter() {
149        if let Some(existing) = consolidated_specs.get_mut(&spec.crate_id) {
150            existing.deps.extend(spec.deps);
151            existing.env.extend(spec.env);
152            existing.aliases.extend(spec.aliases);
153
154            if let Some(source) = &mut existing.source {
155                if let Some(mut new_source) = spec.source {
156                    new_source.exclude_dirs.retain(|src| !source.exclude_dirs.contains(src));
157                    new_source.include_dirs.retain(|src| !source.include_dirs.contains(src));
158                    source.exclude_dirs.extend(new_source.exclude_dirs);
159                    source.include_dirs.extend(new_source.include_dirs);
160                }
161            } else {
162                existing.source = spec.source;
163            }
164
165            existing.cfg.extend(spec.cfg);
166
167            // display_name should match the library's crate name because Rust Analyzer
168            // seems to use display_name for matching crate entries in rust-project.json
169            // against symbols in source files. For more details, see
170            // https://github.com/bazelbuild/rules_rust/issues/1032
171            if spec.crate_type == CrateType::Rlib {
172                existing.display_name = spec.display_name;
173                existing.crate_type = CrateType::Rlib;
174            }
175
176            // We want to use the test target's build label to provide
177            // unit tests codelens actions for library crates in IDEs.
178            if spec.is_test {
179                existing.is_test = true;
180                if let Some(build) = spec.build {
181                    existing.build = Some(build);
182                }
183            }
184
185            // For proc-macro crates that exist within the workspace, there will be a
186            // generated crate-spec in both the fastbuild and opt-exec configuration.
187            // Prefer proc macro paths with an opt-exec component in the path.
188            if let Some(dylib_path) = spec.proc_macro_dylib_path.as_ref() {
189                const OPT_PATH_COMPONENT: &str = "-opt-exec-";
190                if dylib_path.contains(OPT_PATH_COMPONENT) {
191                    existing.proc_macro_dylib_path.replace(dylib_path.clone());
192                }
193            }
194        } else {
195            if let Some(info) = infos.remove(&spec.crate_id) {
196                spec.info = Some(info);
197            }
198            consolidated_specs.insert(spec.crate_id.clone(), spec);
199        }
200    }
201
202    Ok(consolidated_specs.into_values())
203}