]> git.proxmox.com Git - rustc.git/blob - src/tools/rust-analyzer/crates/flycheck/src/lib.rs
bump version to 1.80.1+dfsg1-1~bpo12+pve1
[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 // FIXME: This crate now handles running `cargo test` needed in the test explorer in
6 // addition to `cargo check`. Either split it into 3 crates (one for test, one for check
7 // and one common utilities) or change its name and docs to reflect the current state.
8
9 #![warn(rust_2018_idioms, unused_lifetimes)]
10
11 use std::{fmt, io, process::Command, time::Duration};
12
13 use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
14 use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
15 use rustc_hash::FxHashMap;
16 use serde::Deserialize;
17
18 pub use cargo_metadata::diagnostic::{
19 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
20 DiagnosticSpanMacroExpansion,
21 };
22 use toolchain::Tool;
23
24 mod command;
25 mod test_runner;
26
27 use command::{CommandHandle, ParseFromLine};
28 pub use test_runner::{CargoTestHandle, CargoTestMessage, TestState};
29
30 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
31 pub enum InvocationStrategy {
32 Once,
33 #[default]
34 PerWorkspace,
35 }
36
37 #[derive(Clone, Debug, Default, PartialEq, Eq)]
38 pub enum InvocationLocation {
39 Root(AbsPathBuf),
40 #[default]
41 Workspace,
42 }
43
44 #[derive(Clone, Debug, PartialEq, Eq)]
45 pub struct CargoOptions {
46 pub target_triples: Vec<String>,
47 pub all_targets: bool,
48 pub no_default_features: bool,
49 pub all_features: bool,
50 pub features: Vec<String>,
51 pub extra_args: Vec<String>,
52 pub extra_env: FxHashMap<String, String>,
53 pub target_dir: Option<Utf8PathBuf>,
54 }
55
56 impl CargoOptions {
57 fn apply_on_command(&self, cmd: &mut Command) {
58 for target in &self.target_triples {
59 cmd.args(["--target", target.as_str()]);
60 }
61 if self.all_targets {
62 cmd.arg("--all-targets");
63 }
64 if self.all_features {
65 cmd.arg("--all-features");
66 } else {
67 if self.no_default_features {
68 cmd.arg("--no-default-features");
69 }
70 if !self.features.is_empty() {
71 cmd.arg("--features");
72 cmd.arg(self.features.join(" "));
73 }
74 }
75 if let Some(target_dir) = &self.target_dir {
76 cmd.arg("--target-dir").arg(target_dir);
77 }
78 cmd.envs(&self.extra_env);
79 }
80 }
81
82 #[derive(Clone, Debug, PartialEq, Eq)]
83 pub enum FlycheckConfig {
84 CargoCommand {
85 command: String,
86 options: CargoOptions,
87 ansi_color_output: bool,
88 },
89 CustomCommand {
90 command: String,
91 args: Vec<String>,
92 extra_env: FxHashMap<String, String>,
93 invocation_strategy: InvocationStrategy,
94 invocation_location: InvocationLocation,
95 },
96 }
97
98 impl fmt::Display for FlycheckConfig {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
102 FlycheckConfig::CustomCommand { command, args, .. } => {
103 write!(f, "{command} {}", args.join(" "))
104 }
105 }
106 }
107 }
108
109 /// Flycheck wraps the shared state and communication machinery used for
110 /// running `cargo check` (or other compatible command) and providing
111 /// diagnostics based on the output.
112 /// The spawned thread is shut down when this struct is dropped.
113 #[derive(Debug)]
114 pub struct FlycheckHandle {
115 // XXX: drop order is significant
116 sender: Sender<StateChange>,
117 _thread: stdx::thread::JoinHandle,
118 id: usize,
119 }
120
121 impl FlycheckHandle {
122 pub fn spawn(
123 id: usize,
124 sender: Box<dyn Fn(Message) + Send>,
125 config: FlycheckConfig,
126 sysroot_root: Option<AbsPathBuf>,
127 workspace_root: AbsPathBuf,
128 manifest_path: Option<AbsPathBuf>,
129 ) -> FlycheckHandle {
130 let actor =
131 FlycheckActor::new(id, sender, config, sysroot_root, workspace_root, manifest_path);
132 let (sender, receiver) = unbounded::<StateChange>();
133 let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
134 .name("Flycheck".to_owned())
135 .spawn(move || actor.run(receiver))
136 .expect("failed to spawn thread");
137 FlycheckHandle { id, sender, _thread: thread }
138 }
139
140 /// Schedule a re-start of the cargo check worker to do a workspace wide check.
141 pub fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
142 self.sender.send(StateChange::Restart { package: None, saved_file }).unwrap();
143 }
144
145 /// Schedule a re-start of the cargo check worker to do a package wide check.
146 pub fn restart_for_package(&self, package: String) {
147 self.sender
148 .send(StateChange::Restart { package: Some(package), saved_file: None })
149 .unwrap();
150 }
151
152 /// Stop this cargo check worker.
153 pub fn cancel(&self) {
154 self.sender.send(StateChange::Cancel).unwrap();
155 }
156
157 pub fn id(&self) -> usize {
158 self.id
159 }
160 }
161
162 pub enum Message {
163 /// Request adding a diagnostic with fixes included to a file
164 AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
165
166 /// Request clearing all previous diagnostics
167 ClearDiagnostics { id: usize },
168
169 /// Request check progress notification to client
170 Progress {
171 /// Flycheck instance ID
172 id: usize,
173 progress: Progress,
174 },
175 }
176
177 impl fmt::Debug for Message {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self {
180 Message::AddDiagnostic { id, workspace_root, diagnostic } => f
181 .debug_struct("AddDiagnostic")
182 .field("id", id)
183 .field("workspace_root", workspace_root)
184 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
185 .finish(),
186 Message::ClearDiagnostics { id } => {
187 f.debug_struct("ClearDiagnostics").field("id", id).finish()
188 }
189 Message::Progress { id, progress } => {
190 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
191 }
192 }
193 }
194 }
195
196 #[derive(Debug)]
197 pub enum Progress {
198 DidStart,
199 DidCheckCrate(String),
200 DidFinish(io::Result<()>),
201 DidCancel,
202 DidFailToRestart(String),
203 }
204
205 enum StateChange {
206 Restart { package: Option<String>, saved_file: Option<AbsPathBuf> },
207 Cancel,
208 }
209
210 /// A [`FlycheckActor`] is a single check instance of a workspace.
211 struct FlycheckActor {
212 /// The workspace id of this flycheck instance.
213 id: usize,
214 sender: Box<dyn Fn(Message) + Send>,
215 config: FlycheckConfig,
216 manifest_path: Option<AbsPathBuf>,
217 /// Either the workspace root of the workspace we are flychecking,
218 /// or the project root of the project.
219 root: AbsPathBuf,
220 sysroot_root: Option<AbsPathBuf>,
221 /// CargoHandle exists to wrap around the communication needed to be able to
222 /// run `cargo check` without blocking. Currently the Rust standard library
223 /// doesn't provide a way to read sub-process output without blocking, so we
224 /// have to wrap sub-processes output handling in a thread and pass messages
225 /// back over a channel.
226 command_handle: Option<CommandHandle<CargoCheckMessage>>,
227 /// The receiver side of the channel mentioned above.
228 command_receiver: Option<Receiver<CargoCheckMessage>>,
229
230 status: FlycheckStatus,
231 }
232
233 enum Event {
234 RequestStateChange(StateChange),
235 CheckEvent(Option<CargoCheckMessage>),
236 }
237
238 #[derive(PartialEq)]
239 enum FlycheckStatus {
240 Started,
241 DiagnosticSent,
242 Finished,
243 }
244
245 const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
246
247 impl FlycheckActor {
248 fn new(
249 id: usize,
250 sender: Box<dyn Fn(Message) + Send>,
251 config: FlycheckConfig,
252 sysroot_root: Option<AbsPathBuf>,
253 workspace_root: AbsPathBuf,
254 manifest_path: Option<AbsPathBuf>,
255 ) -> FlycheckActor {
256 tracing::info!(%id, ?workspace_root, "Spawning flycheck");
257 FlycheckActor {
258 id,
259 sender,
260 config,
261 sysroot_root,
262 root: workspace_root,
263 manifest_path,
264 command_handle: None,
265 command_receiver: None,
266 status: FlycheckStatus::Finished,
267 }
268 }
269
270 fn report_progress(&self, progress: Progress) {
271 self.send(Message::Progress { id: self.id, progress });
272 }
273
274 fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
275 if let Ok(msg) = inbox.try_recv() {
276 // give restarts a preference so check outputs don't block a restart or stop
277 return Some(Event::RequestStateChange(msg));
278 }
279 select! {
280 recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
281 recv(self.command_receiver.as_ref().unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
282 }
283 }
284
285 fn run(mut self, inbox: Receiver<StateChange>) {
286 'event: while let Some(event) = self.next_event(&inbox) {
287 match event {
288 Event::RequestStateChange(StateChange::Cancel) => {
289 tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
290 self.cancel_check_process();
291 }
292 Event::RequestStateChange(StateChange::Restart { package, saved_file }) => {
293 // Cancel the previously spawned process
294 self.cancel_check_process();
295 while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
296 // restart chained with a stop, so just cancel
297 if let StateChange::Cancel = restart {
298 continue 'event;
299 }
300 }
301
302 let command =
303 match self.check_command(package.as_deref(), saved_file.as_deref()) {
304 Some(c) => c,
305 None => continue,
306 };
307 let formatted_command = format!("{:?}", command);
308
309 tracing::debug!(?command, "will restart flycheck");
310 let (sender, receiver) = unbounded();
311 match CommandHandle::spawn(command, sender) {
312 Ok(command_handle) => {
313 tracing::debug!(command = formatted_command, "did restart flycheck");
314 self.command_handle = Some(command_handle);
315 self.command_receiver = Some(receiver);
316 self.report_progress(Progress::DidStart);
317 self.status = FlycheckStatus::Started;
318 }
319 Err(error) => {
320 self.report_progress(Progress::DidFailToRestart(format!(
321 "Failed to run the following command: {} error={}",
322 formatted_command, error
323 )));
324 self.status = FlycheckStatus::Finished;
325 }
326 }
327 }
328 Event::CheckEvent(None) => {
329 tracing::debug!(flycheck_id = self.id, "flycheck finished");
330
331 // Watcher finished
332 let command_handle = self.command_handle.take().unwrap();
333 self.command_receiver.take();
334 let formatted_handle = format!("{:?}", command_handle);
335
336 let res = command_handle.join();
337 if let Err(error) = &res {
338 tracing::error!(
339 "Flycheck failed to run the following command: {}, error={}",
340 formatted_handle,
341 error
342 );
343 }
344 if self.status == FlycheckStatus::Started {
345 self.send(Message::ClearDiagnostics { id: self.id });
346 }
347 self.report_progress(Progress::DidFinish(res));
348 self.status = FlycheckStatus::Finished;
349 }
350 Event::CheckEvent(Some(message)) => match message {
351 CargoCheckMessage::CompilerArtifact(msg) => {
352 tracing::trace!(
353 flycheck_id = self.id,
354 artifact = msg.target.name,
355 "artifact received"
356 );
357 self.report_progress(Progress::DidCheckCrate(msg.target.name));
358 }
359
360 CargoCheckMessage::Diagnostic(msg) => {
361 tracing::trace!(
362 flycheck_id = self.id,
363 message = msg.message,
364 "diagnostic received"
365 );
366 if self.status == FlycheckStatus::Started {
367 self.send(Message::ClearDiagnostics { id: self.id });
368 }
369 self.send(Message::AddDiagnostic {
370 id: self.id,
371 workspace_root: self.root.clone(),
372 diagnostic: msg,
373 });
374 self.status = FlycheckStatus::DiagnosticSent;
375 }
376 },
377 }
378 }
379 // If we rerun the thread, we need to discard the previous check results first
380 self.cancel_check_process();
381 }
382
383 fn cancel_check_process(&mut self) {
384 if let Some(command_handle) = self.command_handle.take() {
385 tracing::debug!(
386 command = ?command_handle,
387 "did cancel flycheck"
388 );
389 command_handle.cancel();
390 self.report_progress(Progress::DidCancel);
391 self.status = FlycheckStatus::Finished;
392 }
393 }
394
395 /// Construct a `Command` object for checking the user's code. If the user
396 /// has specified a custom command with placeholders that we cannot fill,
397 /// return None.
398 fn check_command(
399 &self,
400 package: Option<&str>,
401 saved_file: Option<&AbsPath>,
402 ) -> Option<Command> {
403 let (mut cmd, args) = match &self.config {
404 FlycheckConfig::CargoCommand { command, options, ansi_color_output } => {
405 let mut cmd = Command::new(Tool::Cargo.path());
406 if let Some(sysroot_root) = &self.sysroot_root {
407 cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
408 }
409 cmd.arg(command);
410 cmd.current_dir(&self.root);
411
412 match package {
413 Some(pkg) => cmd.arg("-p").arg(pkg),
414 None => cmd.arg("--workspace"),
415 };
416
417 cmd.arg(if *ansi_color_output {
418 "--message-format=json-diagnostic-rendered-ansi"
419 } else {
420 "--message-format=json"
421 });
422
423 if let Some(manifest_path) = &self.manifest_path {
424 cmd.arg("--manifest-path");
425 cmd.arg(manifest_path);
426 if manifest_path.extension().map_or(false, |ext| ext == "rs") {
427 cmd.arg("-Zscript");
428 }
429 }
430
431 options.apply_on_command(&mut cmd);
432 (cmd, options.extra_args.clone())
433 }
434 FlycheckConfig::CustomCommand {
435 command,
436 args,
437 extra_env,
438 invocation_strategy,
439 invocation_location,
440 } => {
441 let mut cmd = Command::new(command);
442 cmd.envs(extra_env);
443
444 match invocation_location {
445 InvocationLocation::Workspace => {
446 match invocation_strategy {
447 InvocationStrategy::Once => {
448 cmd.current_dir(&self.root);
449 }
450 InvocationStrategy::PerWorkspace => {
451 // FIXME: cmd.current_dir(&affected_workspace);
452 cmd.current_dir(&self.root);
453 }
454 }
455 }
456 InvocationLocation::Root(root) => {
457 cmd.current_dir(root);
458 }
459 }
460
461 if args.contains(&SAVED_FILE_PLACEHOLDER.to_owned()) {
462 // If the custom command has a $saved_file placeholder, and
463 // we're saving a file, replace the placeholder in the arguments.
464 if let Some(saved_file) = saved_file {
465 let args = args
466 .iter()
467 .map(|arg| {
468 if arg == SAVED_FILE_PLACEHOLDER {
469 saved_file.to_string()
470 } else {
471 arg.clone()
472 }
473 })
474 .collect();
475 (cmd, args)
476 } else {
477 // The custom command has a $saved_file placeholder,
478 // but we had an IDE event that wasn't a file save. Do nothing.
479 return None;
480 }
481 } else {
482 (cmd, args.clone())
483 }
484 }
485 };
486
487 cmd.args(args);
488 Some(cmd)
489 }
490
491 fn send(&self, check_task: Message) {
492 (self.sender)(check_task);
493 }
494 }
495
496 #[allow(clippy::large_enum_variant)]
497 enum CargoCheckMessage {
498 CompilerArtifact(cargo_metadata::Artifact),
499 Diagnostic(Diagnostic),
500 }
501
502 impl ParseFromLine for CargoCheckMessage {
503 fn from_line(line: &str, error: &mut String) -> Option<Self> {
504 let mut deserializer = serde_json::Deserializer::from_str(line);
505 deserializer.disable_recursion_limit();
506 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
507 return match message {
508 // Skip certain kinds of messages to only spend time on what's useful
509 JsonMessage::Cargo(message) => match message {
510 cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
511 Some(CargoCheckMessage::CompilerArtifact(artifact))
512 }
513 cargo_metadata::Message::CompilerMessage(msg) => {
514 Some(CargoCheckMessage::Diagnostic(msg.message))
515 }
516 _ => None,
517 },
518 JsonMessage::Rustc(message) => Some(CargoCheckMessage::Diagnostic(message)),
519 };
520 }
521
522 error.push_str(line);
523 error.push('\n');
524 None
525 }
526
527 fn from_eof() -> Option<Self> {
528 None
529 }
530 }
531
532 #[derive(Deserialize)]
533 #[serde(untagged)]
534 enum JsonMessage {
535 Cargo(cargo_metadata::Message),
536 Rustc(Diagnostic),
537 }