rust_analyzer_discover/
rust_project.rs

1//! Library for generating rust_project.json files from a `Vec<CrateSpec>`
2//! See official documentation of file format at <https://rust-analyzer.github.io/manual.html>
3
4use core::fmt;
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6use std::str::FromStr;
7
8use anyhow::Context;
9use camino::{Utf8Path, Utf8PathBuf};
10
11use crate::query::{CrateSpec, CrateType};
12use crate::{ToolchainInfo, buildfile_to_targets, source_file_to_buildfile};
13
14/// The argument that `rust-analyzer` can pass to the workspace discovery command.
15#[derive(Clone, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
16#[serde(rename_all = "camelCase")]
17pub enum RustAnalyzerArg {
18    Path(Utf8PathBuf),
19    Buildfile(Utf8PathBuf),
20}
21
22impl RustAnalyzerArg {
23    /// Consumes itself to return a build file and the targets to build.
24    pub fn into_target_details(self, workspace: &Utf8Path) -> anyhow::Result<(Utf8PathBuf, String)> {
25        match self {
26            Self::Path(file) => {
27                let buildfile = source_file_to_buildfile(&file)?;
28                buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t))
29            }
30            Self::Buildfile(buildfile) => buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t)),
31        }
32    }
33}
34
35impl FromStr for RustAnalyzerArg {
36    type Err = anyhow::Error;
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        serde_json::from_str(s).context("rust analyzer argument error")
40    }
41}
42
43/// The format that `rust_analyzer` expects as a response when automatically invoked.
44/// See [rust-analyzer documentation][rd] for a thorough description of this interface.
45///
46/// [rd]: <https://rust-analyzer.github.io/manual.html#rust-analyzer.workspace.discoverConfig>
47#[derive(Debug, serde_derive::Serialize)]
48#[serde(tag = "kind")]
49#[serde(rename_all = "snake_case")]
50pub enum DiscoverProject<'a> {
51    Finished {
52        buildfile: Utf8PathBuf,
53        project: RustProject,
54    },
55    Error {
56        error: String,
57        source: Option<String>,
58    },
59    Progress {
60        message: &'a fmt::Arguments<'a>,
61    },
62}
63
64/// A `rust-project.json` workspace representation. See
65/// [rust-analyzer documentation][rd] for a thorough description of this interface.
66///
67/// [rd]: <https://rust-analyzer.github.io/manual.html#non-cargo-based-projects>
68#[derive(Debug, serde_derive::Serialize)]
69pub struct RustProject {
70    /// The path to a Rust sysroot.
71    sysroot: Utf8PathBuf,
72
73    /// Path to the directory with *source code* of
74    /// sysroot crates.
75    sysroot_src: Utf8PathBuf,
76
77    /// The set of crates comprising the current
78    /// project. Must include all transitive
79    /// dependencies as well as sysroot crate (libstd,
80    /// libcore and such).
81    crates: Vec<Crate>,
82
83    /// The set of runnables, such as tests or benchmarks,
84    /// that can be found in the crate.
85    runnables: Vec<Runnable>,
86}
87
88/// A `rust-project.json` crate representation. See
89/// [rust-analyzer documentation][rd] for a thorough description of this interface.
90///
91/// [rd]: <https://rust-analyzer.github.io/manual.html#non-cargo-based-projects>
92#[derive(Debug, serde_derive::Serialize)]
93pub struct Crate {
94    /// A name used in the package's project declaration
95    #[serde(skip_serializing_if = "Option::is_none")]
96    display_name: Option<String>,
97
98    /// Path to the root module of the crate.
99    root_module: String,
100
101    /// Edition of the crate.
102    edition: String,
103
104    /// Dependencies
105    deps: Vec<Dependency>,
106
107    /// Should this crate be treated as a member of current "workspace".
108    #[serde(skip_serializing_if = "Option::is_none")]
109    is_workspace_member: Option<bool>,
110
111    /// Optionally specify the (super)set of `.rs` files comprising this crate.
112    #[serde(skip_serializing_if = "Source::is_empty")]
113    source: Source,
114
115    /// The set of cfgs activated for a given crate, like
116    /// `["unix", "feature=\"foo\"", "feature=\"bar\""]`.
117    cfg: BTreeSet<String>,
118
119    /// Target triple for this Crate.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    target: Option<String>,
122
123    /// Environment variables, used for the `env!` macro
124    #[serde(skip_serializing_if = "Option::is_none")]
125    env: Option<BTreeMap<String, String>>,
126
127    /// Whether the crate is a proc-macro crate.
128    is_proc_macro: bool,
129
130    /// For proc-macro crates, path to compiled proc-macro (.so file).
131    #[serde(skip_serializing_if = "Option::is_none")]
132    proc_macro_dylib_path: Option<String>,
133
134    /// Build information for the crate
135    #[serde(skip_serializing_if = "Option::is_none")]
136    build: Option<Build>,
137}
138
139#[derive(Debug, Default, serde_derive::Serialize)]
140pub struct Source {
141    include_dirs: Vec<String>,
142    exclude_dirs: Vec<String>,
143}
144
145impl Source {
146    fn is_empty(&self) -> bool {
147        self.include_dirs.is_empty() && self.exclude_dirs.is_empty()
148    }
149}
150
151#[derive(Debug, serde_derive::Serialize)]
152pub struct Dependency {
153    /// Index of a crate in the `crates` array.
154    #[serde(rename = "crate")]
155    crate_index: usize,
156
157    /// The display name of the crate.
158    name: String,
159}
160
161#[derive(Debug, serde_derive::Serialize)]
162pub struct Build {
163    /// The name associated with this crate.
164    ///
165    /// This is determined by the build system that produced
166    /// the `rust-project.json` in question. For instance, if bazel were used,
167    /// the label might be something like `//ide/rust/rust-analyzer:rust-analyzer`.
168    ///
169    /// Do not attempt to parse the contents of this string; it is a build system-specific
170    /// identifier similar to [`Crate::display_name`].
171    label: String,
172    /// Path corresponding to the build system-specific file defining the crate.
173    build_file: Utf8PathBuf,
174    /// The kind of target.
175    ///
176    /// Examples (non-exhaustively) include [`TargetKind::Bin`], [`TargetKind::Lib`],
177    /// and [`TargetKind::Test`]. This information is used to determine what sort
178    /// of runnable codelens to provide, if any.
179    target_kind: TargetKind,
180    runnables: Vec<Runnable>,
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, serde_derive::Serialize)]
184#[serde(rename_all = "camelCase")]
185pub enum TargetKind {
186    Bin,
187    /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
188    Lib,
189    Test,
190}
191
192/// A template-like structure for describing runnables.
193///
194/// These are used for running and debugging binaries and tests without encoding
195/// build system-specific knowledge into rust-analyzer.
196///
197/// # Example
198///
199/// Below is an example of a test runnable. `{label}` and `{test_id}`
200/// are explained in [`Runnable::args`]'s documentation.
201///
202/// ```json
203/// {
204///     "program": "bazel",
205///     "args": [
206///         "test",
207///          "{label}",
208///          "--test_arg",
209///          "{test_id}",
210///     ],
211///     "cwd": "/home/user/repo-root/",
212///     "kind": "testOne"
213/// }
214/// ```
215#[derive(Debug, serde_derive::Serialize)]
216pub struct Runnable {
217    /// The program invoked by the runnable.
218    ///
219    /// For example, this might be `cargo`, `bazel`, etc.
220    program: String,
221    /// The arguments passed to [`Runnable::program`].
222    ///
223    /// The args can contain two template strings: `{label}` and `{test_id}`.
224    /// rust-analyzer will find and replace `{label}` with [`Build::label`] and
225    /// `{test_id}` with the test name.
226    args: Vec<String>,
227    /// The current working directory of the runnable.
228    cwd: Utf8PathBuf,
229    kind: RunnableKind,
230}
231
232/// The kind of runnable.
233#[derive(Debug, Clone, Copy, serde_derive::Serialize)]
234#[serde(rename_all = "camelCase")]
235pub enum RunnableKind {
236    /// Output rustc diagnostics
237    Check,
238
239    /// Can run a binary.
240    Run,
241
242    /// Run a single test.
243    TestOne,
244
245    /// Runs a module of tests
246    TestMod,
247
248    /// Runs a doc test
249    DocTest,
250}
251
252pub fn assemble_rust_project(
253    bazel: &Utf8Path,
254    workspace: &Utf8Path,
255    output_base: &Utf8Path,
256    toolchain_info: ToolchainInfo,
257    crate_specs: impl IntoIterator<Item = CrateSpec>,
258) -> anyhow::Result<RustProject> {
259    let mut project = RustProject {
260        sysroot: toolchain_info.sysroot,
261        sysroot_src: toolchain_info.sysroot_src,
262        crates: Vec::new(),
263        runnables: vec![],
264    };
265
266    let mut all_crates: Vec<_> = crate_specs.into_iter().collect();
267
268    all_crates.sort_by_key(|a| !a.is_test);
269
270    let merged_crates_index: HashMap<_, _> = all_crates.iter().enumerate().map(|(idx, c)| (&c.crate_id, idx)).collect();
271
272    for c in all_crates.iter() {
273        log::trace!("Merging crate {}", &c.crate_id);
274
275        let target_kind = match c.crate_type {
276            CrateType::Bin if c.is_test => TargetKind::Test,
277            CrateType::Bin => TargetKind::Bin,
278            CrateType::Rlib
279            | CrateType::Lib
280            | CrateType::Dylib
281            | CrateType::Cdylib
282            | CrateType::Staticlib
283            | CrateType::ProcMacro => TargetKind::Lib,
284        };
285
286        let mut runnables = Vec::new();
287
288        if let Some(info) = &c.info {
289            if let Some(crate_label) = &info.crate_label
290                && matches!(target_kind, TargetKind::Bin)
291            {
292                runnables.push(Runnable {
293                    program: bazel.to_string(),
294                    args: vec!["run".to_string(), format!("//{crate_label}")],
295                    cwd: workspace.to_owned(),
296                    kind: RunnableKind::Run,
297                });
298            }
299
300            if let Some(test_label) = &info.test_label {
301                runnables.extend([
302                    Runnable {
303                        program: bazel.to_string(),
304                        args: vec![
305                            format!("--output_base={output_base}"),
306                            "test".to_owned(),
307                            format!("//{test_label}"),
308                            "--test_output".to_owned(),
309                            "streamed".to_owned(),
310                            "--test_arg".to_owned(),
311                            "--exact".to_owned(),
312                            "--test_arg".to_owned(),
313                            "{test_id}".to_owned(),
314                        ],
315                        cwd: workspace.to_owned(),
316                        kind: RunnableKind::TestOne,
317                    },
318                    Runnable {
319                        program: bazel.to_string(),
320                        args: vec![
321                            format!("--output_base={output_base}"),
322                            "test".to_owned(),
323                            format!("//{test_label}"),
324                            "--test_output".to_owned(),
325                            "streamed".to_owned(),
326                            "--test_arg".to_owned(),
327                            "{path}".to_owned(),
328                        ],
329                        cwd: workspace.to_owned(),
330                        kind: RunnableKind::TestMod,
331                    },
332                ]);
333            }
334
335            if let Some(doc_test_label) = &info.doc_test_label {
336                runnables.push(Runnable {
337                    program: bazel.to_string(),
338                    args: vec![
339                        format!("--output_base={output_base}"),
340                        "test".to_owned(),
341                        format!("//{doc_test_label}"),
342                        "--test_output".to_owned(),
343                        "streamed".to_owned(),
344                        "--test_arg".to_owned(),
345                        "{test_id}".to_owned(),
346                    ],
347                    cwd: workspace.to_owned(),
348                    kind: RunnableKind::DocTest,
349                });
350            }
351
352            if let Some(clippy_label) = &info.clippy_label {
353                runnables.push(Runnable {
354                    program: bazel.to_string(),
355                    args: vec![
356                        format!("--output_base={output_base}"),
357                        "run".to_owned(),
358                        "--config=wrapper".to_owned(),
359                        "//misc/utils/rust/analyzer/check".to_owned(),
360                        "--".to_owned(),
361                        "--config=wrapper".to_owned(),
362                        format!("//{clippy_label}"),
363                    ],
364                    cwd: workspace.to_owned(),
365                    kind: RunnableKind::Check,
366                });
367            }
368        }
369
370        project.crates.push(Crate {
371            display_name: Some(c.display_name.clone()),
372            root_module: c.root_module.clone(),
373            edition: c.edition.clone(),
374            deps: c
375                .deps
376                .iter()
377                .map(|dep| {
378                    let crate_index = *merged_crates_index
379                        .get(dep)
380                        .expect("failed to find dependency on second lookup");
381                    let dep_crate = &all_crates[crate_index];
382                    let name = if let Some(alias) = c.aliases.get(dep) {
383                        alias.clone()
384                    } else {
385                        dep_crate.display_name.clone()
386                    };
387                    Dependency { crate_index, name }
388                })
389                .collect(),
390            is_workspace_member: Some(c.is_workspace_member),
391            source: match &c.source {
392                Some(s) => Source {
393                    exclude_dirs: s.exclude_dirs.clone(),
394                    include_dirs: s.include_dirs.clone(),
395                },
396                None => Source::default(),
397            },
398            cfg: c.cfg.clone(),
399            target: Some(c.target.clone()),
400            env: Some(c.env.clone()),
401            is_proc_macro: c.proc_macro_dylib_path.is_some(),
402            proc_macro_dylib_path: c.proc_macro_dylib_path.clone(),
403            build: c.build.as_ref().map(|b| Build {
404                label: b.label.clone(),
405                build_file: b.build_file.clone().into(),
406                target_kind,
407                runnables,
408            }),
409        });
410    }
411
412    Ok(project)
413}