]> git.proxmox.com Git - rustc.git/blob - src/tools/rust-analyzer/crates/flycheck/src/lib.rs
New upstream version 1.64.0+dfsg1
[rustc.git] / src / tools / rust-analyzer / crates / flycheck / src / lib.rs
1 //! Flycheck provides the functionality needed to run `cargo check` or
2 //! another compatible command (f.x. clippy) in a background thread and provide
3 //! LSP diagnostics based on the output of the command.
4
5 #![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
6
7 use std::{
8 fmt, io,
9 process::{ChildStderr, ChildStdout, Command, Stdio},
10 time::Duration,
11 };
12
13 use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
14 use paths::AbsPathBuf;
15 use serde::Deserialize;
16 use stdx::{process::streaming_output, JodChild};
17
18 pub use cargo_metadata::diagnostic::{
19 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
20 DiagnosticSpanMacroExpansion,
21 };
22
23 #[derive(Clone, Debug, PartialEq, Eq)]
24 pub enum FlycheckConfig {
25 CargoCommand {
26 command: String,
27 target_triple: Option<String>,
28 all_targets: bool,
29 no_default_features: bool,
30 all_features: bool,
31 features: Vec<String>,
32 extra_args: Vec<String>,
33 },
34 CustomCommand {
35 command: String,
36 args: Vec<String>,
37 },
38 }
39
40 impl fmt::Display for FlycheckConfig {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
44 FlycheckConfig::CustomCommand { command, args } => {
45 write!(f, "{} {}", command, args.join(" "))
46 }
47 }
48 }
49 }
50
51 /// Flycheck wraps the shared state and communication machinery used for
52 /// running `cargo check` (or other compatible command) and providing
53 /// diagnostics based on the output.
54 /// The spawned thread is shut down when this struct is dropped.
55 #[derive(Debug)]
56 pub struct FlycheckHandle {
57 // XXX: drop order is significant
58 sender: Sender<Restart>,
59 _thread: jod_thread::JoinHandle,
60 }
61
62 impl FlycheckHandle {
63 pub fn spawn(
64 id: usize,
65 sender: Box<dyn Fn(Message) + Send>,
66 config: FlycheckConfig,
67 workspace_root: AbsPathBuf,
68 ) -> FlycheckHandle {
69 let actor = FlycheckActor::new(id, sender, config, workspace_root);
70 let (sender, receiver) = unbounded::<Restart>();
71 let thread = jod_thread::Builder::new()
72 .name("Flycheck".to_owned())
73 .spawn(move || actor.run(receiver))
74 .expect("failed to spawn thread");
75 FlycheckHandle { sender, _thread: thread }
76 }
77
78 /// Schedule a re-start of the cargo check worker.
79 pub fn update(&self) {
80 self.sender.send(Restart).unwrap();
81 }
82 }
83
84 pub enum Message {
85 /// Request adding a diagnostic with fixes included to a file
86 AddDiagnostic { workspace_root: AbsPathBuf, diagnostic: Diagnostic },
87
88 /// Request check progress notification to client
89 Progress {
90 /// Flycheck instance ID
91 id: usize,
92 progress: Progress,
93 },
94 }
95
96 impl fmt::Debug for Message {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 Message::AddDiagnostic { workspace_root, diagnostic } => f
100 .debug_struct("AddDiagnostic")
101 .field("workspace_root", workspace_root)
102 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
103 .finish(),
104 Message::Progress { id, progress } => {
105 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
106 }
107 }
108 }
109 }
110
111 #[derive(Debug)]
112 pub enum Progress {
113 DidStart,
114 DidCheckCrate(String),
115 DidFinish(io::Result<()>),
116 DidCancel,
117 }
118
119 struct Restart;
120
121 struct FlycheckActor {
122 id: usize,
123 sender: Box<dyn Fn(Message) + Send>,
124 config: FlycheckConfig,
125 workspace_root: AbsPathBuf,
126 /// CargoHandle exists to wrap around the communication needed to be able to
127 /// run `cargo check` without blocking. Currently the Rust standard library
128 /// doesn't provide a way to read sub-process output without blocking, so we
129 /// have to wrap sub-processes output handling in a thread and pass messages
130 /// back over a channel.
131 cargo_handle: Option<CargoHandle>,
132 }
133
134 enum Event {
135 Restart(Restart),
136 CheckEvent(Option<CargoMessage>),
137 }
138
139 impl FlycheckActor {
140 fn new(
141 id: usize,
142 sender: Box<dyn Fn(Message) + Send>,
143 config: FlycheckConfig,
144 workspace_root: AbsPathBuf,
145 ) -> FlycheckActor {
146 FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
147 }
148 fn progress(&self, progress: Progress) {
149 self.send(Message::Progress { id: self.id, progress });
150 }
151 fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
152 let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
153 select! {
154 recv(inbox) -> msg => msg.ok().map(Event::Restart),
155 recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
156 }
157 }
158 fn run(mut self, inbox: Receiver<Restart>) {
159 while let Some(event) = self.next_event(&inbox) {
160 match event {
161 Event::Restart(Restart) => {
162 // Cancel the previously spawned process
163 self.cancel_check_process();
164 while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}
165
166 let command = self.check_command();
167 tracing::debug!(?command, "will restart flycheck");
168 match CargoHandle::spawn(command) {
169 Ok(cargo_handle) => {
170 tracing::debug!(
171 command = ?self.check_command(),
172 "did restart flycheck"
173 );
174 self.cargo_handle = Some(cargo_handle);
175 self.progress(Progress::DidStart);
176 }
177 Err(error) => {
178 tracing::error!(
179 command = ?self.check_command(),
180 %error, "failed to restart flycheck"
181 );
182 }
183 }
184 }
185 Event::CheckEvent(None) => {
186 tracing::debug!("flycheck finished");
187
188 // Watcher finished
189 let cargo_handle = self.cargo_handle.take().unwrap();
190 let res = cargo_handle.join();
191 if res.is_err() {
192 tracing::error!(
193 "Flycheck failed to run the following command: {:?}",
194 self.check_command()
195 );
196 }
197 self.progress(Progress::DidFinish(res));
198 }
199 Event::CheckEvent(Some(message)) => match message {
200 CargoMessage::CompilerArtifact(msg) => {
201 self.progress(Progress::DidCheckCrate(msg.target.name));
202 }
203
204 CargoMessage::Diagnostic(msg) => {
205 self.send(Message::AddDiagnostic {
206 workspace_root: self.workspace_root.clone(),
207 diagnostic: msg,
208 });
209 }
210 },
211 }
212 }
213 // If we rerun the thread, we need to discard the previous check results first
214 self.cancel_check_process();
215 }
216
217 fn cancel_check_process(&mut self) {
218 if let Some(cargo_handle) = self.cargo_handle.take() {
219 cargo_handle.cancel();
220 self.progress(Progress::DidCancel);
221 }
222 }
223
224 fn check_command(&self) -> Command {
225 let mut cmd = match &self.config {
226 FlycheckConfig::CargoCommand {
227 command,
228 target_triple,
229 no_default_features,
230 all_targets,
231 all_features,
232 extra_args,
233 features,
234 } => {
235 let mut cmd = Command::new(toolchain::cargo());
236 cmd.arg(command);
237 cmd.current_dir(&self.workspace_root);
238 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
239 .arg(self.workspace_root.join("Cargo.toml").as_os_str());
240
241 if let Some(target) = target_triple {
242 cmd.args(&["--target", target.as_str()]);
243 }
244 if *all_targets {
245 cmd.arg("--all-targets");
246 }
247 if *all_features {
248 cmd.arg("--all-features");
249 } else {
250 if *no_default_features {
251 cmd.arg("--no-default-features");
252 }
253 if !features.is_empty() {
254 cmd.arg("--features");
255 cmd.arg(features.join(" "));
256 }
257 }
258 cmd.args(extra_args);
259 cmd
260 }
261 FlycheckConfig::CustomCommand { command, args } => {
262 let mut cmd = Command::new(command);
263 cmd.args(args);
264 cmd
265 }
266 };
267 cmd.current_dir(&self.workspace_root);
268 cmd
269 }
270
271 fn send(&self, check_task: Message) {
272 (self.sender)(check_task);
273 }
274 }
275
276 /// A handle to a cargo process used for fly-checking.
277 struct CargoHandle {
278 /// The handle to the actual cargo process. As we cannot cancel directly from with
279 /// a read syscall dropping and therefor terminating the process is our best option.
280 child: JodChild,
281 thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
282 receiver: Receiver<CargoMessage>,
283 }
284
285 impl CargoHandle {
286 fn spawn(mut command: Command) -> std::io::Result<CargoHandle> {
287 command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
288 let mut child = JodChild::spawn(command)?;
289
290 let stdout = child.stdout.take().unwrap();
291 let stderr = child.stderr.take().unwrap();
292
293 let (sender, receiver) = unbounded();
294 let actor = CargoActor::new(sender, stdout, stderr);
295 let thread = jod_thread::Builder::new()
296 .name("CargoHandle".to_owned())
297 .spawn(move || actor.run())
298 .expect("failed to spawn thread");
299 Ok(CargoHandle { child, thread, receiver })
300 }
301
302 fn cancel(mut self) {
303 let _ = self.child.kill();
304 let _ = self.child.wait();
305 }
306
307 fn join(mut self) -> io::Result<()> {
308 let _ = self.child.kill();
309 let exit_status = self.child.wait()?;
310 let (read_at_least_one_message, error) = self.thread.join()?;
311 if read_at_least_one_message || exit_status.success() {
312 Ok(())
313 } else {
314 Err(io::Error::new(io::ErrorKind::Other, format!(
315 "Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
316 exit_status, error
317 )))
318 }
319 }
320 }
321
322 struct CargoActor {
323 sender: Sender<CargoMessage>,
324 stdout: ChildStdout,
325 stderr: ChildStderr,
326 }
327
328 impl CargoActor {
329 fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
330 CargoActor { sender, stdout, stderr }
331 }
332
333 fn run(self) -> io::Result<(bool, String)> {
334 // We manually read a line at a time, instead of using serde's
335 // stream deserializers, because the deserializer cannot recover
336 // from an error, resulting in it getting stuck, because we try to
337 // be resilient against failures.
338 //
339 // Because cargo only outputs one JSON object per line, we can
340 // simply skip a line if it doesn't parse, which just ignores any
341 // erroneus output.
342
343 let mut error = String::new();
344 let mut read_at_least_one_message = false;
345 let output = streaming_output(
346 self.stdout,
347 self.stderr,
348 &mut |line| {
349 read_at_least_one_message = true;
350
351 // Try to deserialize a message from Cargo or Rustc.
352 let mut deserializer = serde_json::Deserializer::from_str(line);
353 deserializer.disable_recursion_limit();
354 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
355 match message {
356 // Skip certain kinds of messages to only spend time on what's useful
357 JsonMessage::Cargo(message) => match message {
358 cargo_metadata::Message::CompilerArtifact(artifact)
359 if !artifact.fresh =>
360 {
361 self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
362 }
363 cargo_metadata::Message::CompilerMessage(msg) => {
364 self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
365 }
366 _ => (),
367 },
368 JsonMessage::Rustc(message) => {
369 self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
370 }
371 }
372 }
373 },
374 &mut |line| {
375 error.push_str(line);
376 error.push('\n');
377 },
378 );
379 match output {
380 Ok(_) => Ok((read_at_least_one_message, error)),
381 Err(e) => Err(io::Error::new(e.kind(), format!("{:?}: {}", e, error))),
382 }
383 }
384 }
385
386 enum CargoMessage {
387 CompilerArtifact(cargo_metadata::Artifact),
388 Diagnostic(Diagnostic),
389 }
390
391 #[derive(Deserialize)]
392 #[serde(untagged)]
393 enum JsonMessage {
394 Cargo(cargo_metadata::Message),
395 Rustc(Diagnostic),
396 }