rust_analyzer/
discover.rs

1//! Infrastructure for lazy project discovery. Currently only support rust-project.json discovery
2//! via a custom discover command.
3use std::{io, path::Path};
4
5use crossbeam_channel::Sender;
6use ide_db::FxHashMap;
7use paths::{AbsPathBuf, Utf8Path, Utf8PathBuf};
8use project_model::ProjectJsonData;
9use serde::{Deserialize, Serialize};
10use tracing::{info_span, span::EnteredSpan};
11
12use crate::command::{CommandHandle, JsonLinesParser};
13
14pub(crate) const ARG_PLACEHOLDER: &str = "{arg}";
15
16/// A command wrapper for getting a `rust-project.json`.
17///
18/// This is analogous to discovering a cargo project + running `cargo-metadata` on it, but for non-Cargo build systems.
19pub(crate) struct DiscoverCommand {
20    command: Vec<String>,
21    sender: Sender<DiscoverProjectMessage>,
22}
23
24#[derive(PartialEq, Clone, Debug, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub(crate) enum DiscoverArgument {
27    Path(#[serde(serialize_with = "serialize_abs_pathbuf")] AbsPathBuf),
28    Buildfile(#[serde(serialize_with = "serialize_abs_pathbuf")] AbsPathBuf),
29}
30
31fn serialize_abs_pathbuf<S>(path: &AbsPathBuf, se: S) -> Result<S::Ok, S::Error>
32where
33    S: serde::Serializer,
34{
35    let path: &Utf8Path = path.as_ref();
36    se.serialize_str(path.as_str())
37}
38
39impl DiscoverCommand {
40    /// Create a new [DiscoverCommand].
41    pub(crate) fn new(sender: Sender<DiscoverProjectMessage>, command: Vec<String>) -> Self {
42        Self { sender, command }
43    }
44
45    /// Spawn the command inside [Discover] and report progress, if any.
46    pub(crate) fn spawn(
47        &self,
48        discover_arg: DiscoverArgument,
49        current_dir: &Path,
50    ) -> io::Result<DiscoverHandle> {
51        let command = &self.command[0];
52        let args = &self.command[1..];
53
54        let args: Vec<String> = args
55            .iter()
56            .map(|arg| {
57                if arg == ARG_PLACEHOLDER {
58                    serde_json::to_string(&discover_arg).expect("Unable to serialize args")
59                } else {
60                    arg.to_owned()
61                }
62            })
63            .collect();
64
65        // TODO: are we sure the extra env should be empty?
66        let mut cmd = toolchain::command(command, current_dir, &FxHashMap::default());
67        cmd.args(args);
68
69        Ok(DiscoverHandle {
70            handle: CommandHandle::spawn(cmd, DiscoverProjectParser, self.sender.clone(), None)?,
71            span: info_span!("discover_command").entered(),
72        })
73    }
74}
75
76/// A handle to a spawned [Discover].
77#[derive(Debug)]
78pub(crate) struct DiscoverHandle {
79    pub(crate) handle: CommandHandle<DiscoverProjectMessage>,
80    #[allow(dead_code)] // not accessed, but used to log on drop.
81    span: EnteredSpan,
82}
83
84/// An enum containing either progress messages, an error,
85/// or the materialized `rust-project`.
86#[derive(Debug, Clone, Deserialize, Serialize)]
87#[serde(tag = "kind")]
88#[serde(rename_all = "snake_case")]
89enum DiscoverProjectData {
90    Finished { buildfile: Utf8PathBuf, project: ProjectJsonData },
91    Error { error: String, source: Option<String> },
92    Progress { message: String },
93}
94
95#[derive(Debug, PartialEq, Clone)]
96pub(crate) enum DiscoverProjectMessage {
97    Finished { project: ProjectJsonData, buildfile: AbsPathBuf },
98    Error { error: String, source: Option<String> },
99    Progress { message: String },
100}
101
102impl DiscoverProjectMessage {
103    fn new(data: DiscoverProjectData) -> Self {
104        match data {
105            DiscoverProjectData::Finished { project, buildfile, .. } => {
106                let buildfile = buildfile.try_into().expect("Unable to make path absolute");
107                DiscoverProjectMessage::Finished { project, buildfile }
108            }
109            DiscoverProjectData::Error { error, source } => {
110                DiscoverProjectMessage::Error { error, source }
111            }
112            DiscoverProjectData::Progress { message } => {
113                DiscoverProjectMessage::Progress { message }
114            }
115        }
116    }
117}
118
119struct DiscoverProjectParser;
120
121impl JsonLinesParser<DiscoverProjectMessage> for DiscoverProjectParser {
122    fn from_line(&self, line: &str, _error: &mut String) -> Option<DiscoverProjectMessage> {
123        match serde_json::from_str::<DiscoverProjectData>(line) {
124            Ok(data) => {
125                let msg = DiscoverProjectMessage::new(data);
126                Some(msg)
127            }
128            Err(err) => {
129                let err =
130                    DiscoverProjectData::Error { error: format!("{err:#?}\n{line}"), source: None };
131                Some(DiscoverProjectMessage::new(err))
132            }
133        }
134    }
135
136    fn from_eof(&self) -> Option<DiscoverProjectMessage> {
137        None
138    }
139}
140
141#[test]
142fn test_deserialization() {
143    let message = r#"
144    {"kind": "progress", "message":"querying build system","input":{"files":["src/main.rs"]}}
145    "#;
146    let message: DiscoverProjectData =
147        serde_json::from_str(message).expect("Unable to deserialize message");
148    assert!(matches!(message, DiscoverProjectData::Progress { .. }));
149
150    let message = r#"
151    {"kind": "error", "error":"failed to deserialize command output","source":"command"}
152    "#;
153
154    let message: DiscoverProjectData =
155        serde_json::from_str(message).expect("Unable to deserialize message");
156    assert!(matches!(message, DiscoverProjectData::Error { .. }));
157
158    let message = r#"
159    {"kind": "finished", "project": {"sysroot": "foo", "crates": [], "runnables": []}, "buildfile":"rust-analyzer/BUILD"}
160    "#;
161
162    let message: DiscoverProjectData =
163        serde_json::from_str(message).expect("Unable to deserialize message");
164    assert!(matches!(message, DiscoverProjectData::Finished { .. }));
165}